注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

停止编写 API 函数

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。 RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的...
继续阅读 »

如果你正在开发一个前端程序,后端使用的 RESTFUL API,那你必须停止为每一个接口编写函数。


RESTFUL API 通常提供在不同实体上执行增删改查(CRUD)操作的一组接口。我们通常在我们的前端项目中为这些每一个接口提供一个函数,这些函数的功能非常的相似,只是为了服务于不用的实体。举个例子,假设我们有这些函数。

// api/users.js

// 创建
export function createUser(userFormValues) {
return fetch('users', { method: 'POST', body: userFormValues });
}

// 查询
export function getListOfUsers(keyword) {
return fetch(`/users?keyword=${keyword}`);
}

export function getUser(id) {
return fetch(`/users/${id}`);
}

// 更新
export updateUser(id, userFormValues) {
return fetch(`/users/${is}`, { method: 'PUT', body: userFormValues });
}

// 删除
export function removeUser(id) {
return fetch(`/users/${id}`, { method: 'DELETE' });
}

类似的功能可能存在于其他实体,例如:城市、产品、类别...但是我们可以用一个简单的函数调用来代替这些函数:

// apis/users.js
export const users = crudBuilder('/users');

// apis/cities.js
export const cities = crudBuilder('/regions/cities');


然后像这样去使用:

users.create(values);
users.show(1);
users.list('john');
users.update(values);
users.remove(1);

你可能会问为什么?有一些很好的理由:


  • 减少了代码行数:你编写的代码,和当你离开公司时其他人维护的代码
  • 强制执行 API 函数的命名约定,这可以增加代码的可读性和可维护性。例如你已经见过的函数名称: getListOfUsersgetCitiesgetAllProductsproductIndexfetchCategories等, 他们都在做相同的事情,那就是“获取实体列表”。使用这种方法,你将始终拥有entityName.list()函数,并且团队中的每个人都知道这一点。

所以,让我们创建crudBuilder()函数,然后再添加一些糖。


一个非常简单的 CRUD 构造器


对于上边的简单示例,crudBuilder()函数将非常简单:

export function crudBuilder(baseRoute) {
function list(keyword) {
return fetch(`${baseRoute}?keyword=${keyword}`);
}
function show(id) {
return fetch(`${baseRoute}/${id}`);
}
function create(formValues) {
return fetch(baseRoute, { method: 'POST', body: formValues });
}
function update(id, formValues) {
return fetch(`${baseRoute}/${id}`, { method: 'PUT', body: formValues });
}
function remove(id) {
return fetch(`${baseRoute}/${id}`, { method: 'DELETE' });
}

return {
list,
show,
create,
update,
remove
};
}

假设约定 API 路径并且给相应实体提供一个路径前缀,他将返回该实体上调用 CRUD 操作所需的所有方法。


但老实说,我们知道现实世界的应用程序并不会那么简单。在将这种方法应用于我们的项目时,有很多事情需要考虑:


  • 过滤:列表 API 通常会提供许多过滤器参数
  • 分页:列表 API 总是分页的
  • 转换:API 返回的值在实际使用之前可能需要进行一些转换
  • 准备:formValues对象在发送给 API 之前需要做一些准备工作
  • 自定义接口:更新特定项的接口不总是${baseRoute}/${id}

因此,我们需要可以处理更多复杂场景的 CRUD 构造器。


高级 CRUD 构造器


让我们通过上述方法来构建一些日常中我们真正使用的东西。


过滤


首先,我们应该在 list输出函数中处理更加复杂的过滤。每个实体列表可能有不同的过滤器并且用户可能应用了其中的一些过滤器。因此,我们不能对应用过滤器的形状和值有任何假设,但是我们可以假设任何列表过滤都可以产生一个对象,该对象为不同的过滤器名称指定了一些值。例如,我们可以过滤一些用户:

const filters = {
keyword: 'john',
createdAt: new Date('2020-02-10')
};

另一方面,我们不知道这些过滤器应该如何传递给 API,但是我们可以假设(跟 API 提供方进行约定)每一个过滤器在列表 API 中都有一个相应的参数,可以以'key=value'URL 查询参数的形式被传递。


因此我们需要知道如何将应用的过滤器转换成相对应的 API 参数来创建我们的 list 函数。这可以通过将 transformFilters 参数传递给 crudBuilder() 来完成。举一个用户的例子:

function transformUserFilters(filters) {
const params = [];
if (filters.keyword) {
params.push(`keyword=${filters.keyword}`);
}
if (filters.createdAt) {
params.push(`create_at=${dateUtility.format(filters.createdAt)}`);
}

return params;
}

现在我们可以使用这个参数来创建 list 函数了。

export function crudBuilder(baseRoute, transformFilters) {
function list(filters) {
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${baseRoute}${params}`);
}
}

转换和分页


从 API 接收的数据可能需要进行一些转换才能在我们的应用程序中使用。例如,我们可能需要将 snake_case 转换成驼峰命名或将一些日期字符串转换成用户时区。


此外,我们还需要处理分页。


我们假设来自 API 的分页数据都按照如下格式(与 API 提供者约定):

{
data: [], // 实体对象列表
pagination: {...} // 分页信息
}

因此,我们需要知道如何转换单个实体对象。然后我们可以遍历列表对象来转换他们。为此,我们需要一个 transformEntity 函数作为 crudBuilder 的参数。

export function crudBuilder(baseRoute, transformFilters, transformEntity, ) {
function list(filters) {
const params = transformFilters(filters)?.join('&');
return fetch(`${baseRoute}?${params}`)
.then((res) => res.json())
.then((res) => ({
data: res.data.map((entity) => transformEntity(entity)),
pagination: res.pagination
}));
}
}

list() 函数我们就完成了。


准备


对于 createupdate 函数,我们需要将 formValues 转换成 API 需要的格式。例如,假设我们在表单中有一个 City 的城市选择对象。但是 create API 只需要 city_id。因此,我们需要一个执行以下操作的函数:

const prepareValue = formValue => ({city_id: formValues.city.id});

这个函数会根据用例返回普通对象或者 FormData,并且可以将数据传递给 API:

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues) {
function create(formValues) {
return fetch(baseRoute, {
method: 'POST',
body: prepareFormValues(formValues)
});
}
}

自定义接口


在一些少数情况下,对实体执行某些操作的 API 接口不遵循相同的约定。例如,我们不能使用 /users/${id} 来编辑用户,而是使用 /edit-user/${id}。对于这些情况,我们应该指定一个自定义路径。


在这里我们允许覆盖 crud builder 中使用的任何路径。注意,展示、更新、移除操作的路径可能取决于具体实体对象的信息,因此我们必须使用函数并传递实体对象来获取路径。


我们需要在对象中获取这些自定义路径,如果没有指定,就退回到默认路径。像这样:

const paths = {
list: 'list-of-users',
show: (userId) => `users/with/id/${userId}`,
create: 'users/new',
update: (user) => `users/update/${user.id}`,
remove: (user) => `delete-user/${user.id}`
};

最终的 BRUD 构造器


这是创建 CRUD 函数的最终代码。

export function crudBuilder(baseRoute, transformFilters, transformEntity, prepareFormValues, paths) {
function list (filters) {
const path = paths.list || baseRoute;
let params = transformFilters(filters)?.join('&');
if (params) {
params += '?';
}

return fetch(`${path}${params}`)
.then((res) => res.json())
.then(() => ({
data: res.data.map(entity => transformEntity(entity)),
pagination: res.pagination
}));
}
function show(id) {
const path = paths.show?.(id) || `${baseRoute}/${id}`;

return fetch(path)
.then((res) => res.json())
.then((res => transformEntity(res)));
}
function create(formValues) {
const path = paths.create || baseRoute;

return fetch(path, { method: 'POST', body: prepareFormValues(formValues) });
}
function update(id, formValues) {
const path = paths.update?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'PUT', body: formValues });
}
function remove(id) {
const path = paths.remove?.(id) || `${baseRoute}/${id}`;

return fetch(path, { method: 'DELETE' });
}
return {
list,
show,
create,
update,
remove
}
}


Saeed Mosavat: Stop writing API functions


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

安卓开发中如何实现一个定时任务

定时任务方式优点缺点使用场景所用的API普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)Timer定时器简单易用,可以设置固定周期或者...
继续阅读 »

定时任务方式优点缺点使用场景所用的API
普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)
Timer定时器简单易用,可以设置固定周期或者延迟执行的任务不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Timer.schedule(TimerTask,long)
ScheduledExecutorService灵活强大,可以设置固定周期或者延迟执行的任务,并支持多线程并发不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要多线程并发的定时任务Executors.newScheduledThreadPool(int).schedule(Runnable,long,TimeUnit)
Handler中的postDelayed方法简单易用,可以设置延迟执行的任务,并与UI线程交互不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要与UI线程交互的定时任务Handler.postDelayed(Runnable,long)
Service + AlarmManger + BroadcastReceiver可靠稳定,可以设置精确或者不精确的闹钟,并在后台长期运行需要声明相关权限,并受系统时间影响需要在App外部执行长期且对时间敏感的定时任务AlarmManager.set(int,PendingIntent), BroadcastReceiver.onReceive(Context,Intent), Service.onStartCommand(Intent,int,int)
WorkManager可靠稳定,不受系统时间影响,并可以设置多种约束条件来执行任务需要添加依赖,并不能保证准时执行需要在App外部执行长期且对时间不敏感且需要满足特定条件才能执行的定时任务WorkManager.enqueue(WorkRequest), Worker.doWork()
RxJava简洁、灵活、支持多线程、支持背压、支持链式操作学习曲线较高、内存占用较大需要处理复杂的异步逻辑或数据流io.reactivex:rxjava:2.2.21
CountDownTimer简单易用、不需要额外的线程或handler不支持取消或重置倒计时、精度受系统时间影响需要实现简单的倒计时功能android.os.CountDownTimer
协程+Flow语法简洁、支持协程作用域管理生命周期、支持流式操作和背压需要引入额外的依赖库、需要熟悉协程和Flow的概念和用法需要处理异步数据流或响应式编程kotlinx-coroutines-core:1.5.0
使用downTo关键字和Flow实现一个定时任务1、可以使用简洁的语法创建一个倒数的范围 2 、可以使用Flow异步地发射和收集倒数的值3、可以使用onEach等操作符对倒数的值进行处理或转换1、需要注意倒数的范围是否包含0,否则可能会出现偏差 2、需要注意倒数的间隔是否与delay函数的参数一致,否则可能会出现不准确 3、需要注意取消或停止Flow的时机,否则可能会出现内存泄漏或资源浪费1、适合于需要实现简单的倒计时功能,例如显示剩余时间或进度 2、适合于需要在倒计时过程中执行一些额外的操作,例如播放声音或更新UI 3、适合于需要在倒计时结束后执行一些额外的操作,例如跳转页面或弹出对话框implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
Kotlin 内联函数的协程和 Flow 实现很容易离开主线程,样板代码最少,协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。可以处理大量的异步数据,而不会阻塞主线程。可能会导致内存泄漏和性能问题。处理 I/O 阻塞型操作,而不是计算密集型操作。kotlinx.coroutines 和 kotlinx.coroutines.flow

安卓开发中如何实现一个定时任务


在安卓开发中,我们经常会遇到需要定时执行某些任务的需求,比如轮询服务器数据、更新UI界面、发送通知等等。那么,我们该如何实现一个定时任务呢?本文将介绍安卓开发中实现定时任务的五种方式,并比较它们的优缺点,以及适用场景。


1. 普通线程sleep的方式


这种方式是最简单也最直观的一种实现方法,就是在一个普通线程中使用sleep方法来延迟执行某个任务。例如:


// 创建一个普通线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 循环执行
while (true) {
// 执行某个任务
doSomething();
// 延迟10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 启动线程
thread.start();


这种方式的优点是简单易懂,不需要借助其他类或组件。但是它也有很多缺点:



  • sleep方法会阻塞当前线程,导致资源浪费和性能下降。

  • sleep方法不准确,它只能保证在指定时间后醒来,但不能保证立即执行。

  • sleep方法受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • sleep方法不可靠,如果线程被异常终止或者进入休眠状态,会导致计时中断。


因此,这种方式只适合一般的轮询Polling场景。


2. Timer定时器


这种方式是使用Java API里提供的Timer类来实现定时任务。Timer类可以创建一个后台线程,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Timer对象
Timer timer = new Timer();
// 创建一个TimerTask对象
TimerTask task = new TimerTask() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
timer.schedule(task, 5000, 10000);


这种方式相比第一种方式有以下优点:



  • Timer类内部使用wait和notify方法来控制线程的执行和休眠,不会浪费资源和性能。

  • Timer类可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • Timer类可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • Timer类只创建了一个后台线程来执行所有的任务,如果其中一个任务耗时过长或者出现异常,则会影响其他任务的执行。

  • Timer类受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • Timer类不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些不太重要的定时任务。


3. ScheduledExecutorService


这种方式是使用Java并发包里提供的ScheduledExecutorService接口来实现定时任务。ScheduledExecutorService接口可以创建一个线程池,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个ScheduledExecutorService对象
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
service.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS);


这种方式相比第二种方式有以下优点:



  • ScheduledExecutorService接口可以创建多个线程来执行多个任务,避免了单线程的弊端。

  • ScheduledExecutorService接口可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • ScheduledExecutorService接口可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • ScheduledExecutorService接口受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • ScheduledExecutorService接口不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些需要多线程并发执行的定时任务。


4. Handler中的postDelayed方法


这种方式是使用Android API里提供的Handler类来实现定时任务。Handler类可以在主线程或者子线程中发送和处理消息,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Handler对象
Handler handler = new Handler();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
// 延迟10秒后再次执行该任务
handler.postDelayed(this, 10000);
}
};
// 延迟5秒后开始执行该任务
handler.postDelayed(task, 5000);


这种方式相比第三种方式有以下优点:



  • Handler类不受系统时间影响,它使用系统启动时间作为参考。

  • Handler类可以在主线程中更新UI界面,避免了线程间通信的问题。


但是这种方式也有以下缺点:



  • Handler类只能在当前进程中使用,如果进程被杀死或者进入休眠状态,会导致计时中断。

  • Handler类需要手动循环调用postDelayed方法来实现周期性地执行任务。


因此,这种方式适合一些需要在主线程中更新UI界面的定时任务.


5. Service + AlarmManager + BroadcastReceiver


这种方式是使用Android API里提供的三个组件来实现定时任务. Service组件可以在后台运行某个长期的服务;AlarmManager组件可以设置一个闹钟,在指定的时间发送一个



  • Intent,用于指定要启动的Service组件和传递一些参数。

  • AlarmManager组件可以设置一个闹钟,在指定的时间发送一个Intent给BroadcastReceiver组件。

  • BroadcastReceiver组件可以接收AlarmManager发送的Intent,并启动Service组件来执行任务。


这种方式相比第四种方式有以下优点:



  • Service组件可以在后台运行,即使进程被杀死或者进入休眠状态,也不会影响计时。

  • AlarmManager组件可以设置精确或者不精确的闹钟,根据不同的需求节省电量。

  • BroadcastReceiver组件可以在系统开机后自动注册,实现开机自启动。


但是这种方式也有以下缺点:



  • Service组件需要在AndroidManifest.xml文件中声明,并申请相关的权限。

  • AlarmManager组件受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • BroadcastReceiver组件需要在代码中动态注册和注销,避免内存泄漏。


因此,这种方式适合一些需要长期在后台执行的定时任务。


6. WorkManager


这种方式是使用Android Jetpack里提供的WorkManager库来实现定时任务. WorkManager库是一个用于管理后台任务的框架,它可以在满足一定条件下执行某个任务,并保证任务一定会被执行。例如:


// 创建一个PeriodicWorkRequest对象
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
// 获取一个WorkManager对象
WorkManager workManager = WorkManager.getInstance(this);
// 将PeriodicWorkRequest对象加入到队列中
workManager.enqueue(request);


这种方式相比第五种方式有以下优点:



  • WorkManager库不受系统时间影响,它使用系统启动时间作为参考。

  • WorkManager库可以设置多种约束条件来执行任务,例如网络状态、电量状态、设备空闲状态等。

  • WorkManager库可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • WorkManager库需要添加依赖并配置相关的权限。

  • WorkManager库不能保证任务准时执行,它会根据系统资源和约束条件来调度任务。


因此,这种方式适合一些对时间不敏感且需要满足特定条件才能执行的定时任务


您好,这是必应。我根据您的要求,从网络上搜集了一些关于如何使用RXJAVA、CountDownTimer、协程安卓实现一个定时任务的资料,并整理成了一篇文章。文章的结构如下:


7.如何使用RXJAVA实现定时任务


RXJAVA是一个基于观察者模式和函数式编程的库,它可以用来处理异步事件和数据流。RXJAVA提供了一些操作符,可以用来创建和处理定时任务。其中,interval操作符可以用来创建一个按照固定时间间隔发射整数序列的Observable,可以用来实现周期性的定时任务。例如,以下代码可以实现每隔1秒发射一个整数,从0开始:


//创建一个Observable,每隔1秒发射一个整数,从0开始
Observable timer = Observable.interval(1, TimeUnit.SECONDS);
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


如果要实现倒计时的效果,可以使用map操作符,将发射的整数转换为倒计时的秒数。例如,以下代码可以实现一个10秒的倒计时:


//创建一个Observable,每隔1秒发射一个整数,从0开始,共发射10个
Observable timer = Observable.interval(1, TimeUnit.SECONDS).take(10);
//使用map操作符,将发射的整数转换为倒计时的秒数
timer = timer.map(new Function() {
@Override
public Long apply(Long aLong) throws Exception {
//将发射的整数转换为倒计时的秒数,例如0转换为10,1转换为9,以此类推
return 10 - aLong;
}
});
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


RXJAVA的优点是可以方便地处理异步事件和数据流,可以使用各种操作符来实现复杂的逻辑,可以避免内存泄漏和线程安全


8.如何使用CountDownTimer实现定时任务


CountDownTimer是Android中提供的一个倒计时器类,它可以用来实现一个在一定时间内递减的倒计时。CountDownTimer的构造方法接受两个参数:总时间和间隔时间。例如,以下代码可以创建一个10秒的倒计时,每隔1秒更新一次:


//创建一个10秒的倒计时,每隔1秒更新一次
CountDownTimer timer = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//每隔一秒调用一次,可以用来更新UI或者执行其他逻辑
Log.d("CountDownTimer", "onTick: " + millisUntilFinished / 1000);
}

@Override
public void onFinish() {
//倒计时结束时调用,可以用来释放资源或者执行其他逻辑
Log.d("CountDownTimer", "onFinish");
}
};
//开始倒计时
timer.start();
//取消倒计时
timer.cancel();


CountDownTimer的优点是使用简单,可以直接在UI线程中更新UI,不需要额外的线程或者Handler。CountDownTimer的缺点是只能实现倒计时的效果,不能实现周期性的定时任务,而且精度受系统时间的影响,可能不够准确。


9.如何使用协程实现定时任务


协程可以用来简化异步编程和线程管理。协程是一种轻量级的线程,它可以在不阻塞线程的情况下挂起和恢复执行。协程安卓提供了一些扩展函数,可以用来创建和处理定时任务。其中,delay函数可以用来暂停协程的执行一段时间,可以用来实现倒计时或者周期性的定时任务。例如,以下代码可以实现一个10秒的倒计时,每隔1秒更新一次:


//创建一个协程作用域,可以用来管理协程的生命周期
val scope = CoroutineScope(Dispatchers.Main)
//在协程作用域中启动一个协程,可以用来执行异步任务
scope.launch {
//创建一个变量,表示倒计时的秒数
var seconds = 10
//循环执行,直到秒数为0
while (seconds > 0) {
//打印秒数,可以用来更新UI或者执行其他逻辑
Log.d("Coroutine", "seconds: $seconds")
//暂停协程的执行1秒,不阻塞线程
delay(1000)
//秒数减一
seconds--
}
//倒计时结束,打印日志,可以用来释放资源或者执行其他逻辑
Log.d("Coroutine", "finish")
}
//取消协程作用域,可以用来取消所有的协程
scope.cancel()


协程安卓的优点是可以方便地处理异步任务和线程切换,可以使用简洁的语法来实现复杂的逻辑,可以避免内存泄漏和回调。协程的缺点是需要引入额外的依赖,而且需要一定的学习成本,不太适合初学者。


10.使用kotlin关键字 ‘downTo’ 搭配Flow


// 创建一个倒计时器,从10秒开始,每秒减一
val timer = object: CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// 在每个间隔,发射剩余的秒数
emitSeconds(millisUntilFinished / 1000)
}

override fun onFinish() {
// 在倒计时结束时,发射0
emitSeconds(0)
}
}

// 创建一个Flow,用于发射倒数的秒数
fun emitSeconds(seconds: Long): Flow = flow {
// 使用downTo关键字创建一个倒数的范围
for (i in seconds downTo 0) {
// 发射当前的秒数
emit(i.toInt())
}
}


11.kotlin内联函数的协程和 Flow 实现


fun FragmentActivity.timerFlow(
time: Int = 60,
onStart: (suspend () -> Unit)? = null,
onEach: (suspend (Int) -> Unit)? =
null,
onCompletion: (suspend () -> Unit)? =
null
): Job {
return (time downTo 0)
.asFlow()
.cancellable()
.flowOn(Dispatchers.Default)
.onStart { onStart?.invoke() }
.onEach {
onEach?.invoke(it)
delay(
1000L)
}.onCompletion { onCompletion?.invoke() }
.launchIn(lifecycleScope)
}


//在activity中使用
val job = timerFlow(
time = 60,
onStart = { Log.d("Timer", "Starting timer...") },
onEach = { Log.d("Timer", "Seconds remaining: $it") },
onCompletion = { Log.d("Timer", "Timer completed.") }
)

//取消计时
job.cancel()

作者:淘淘养乐多
来源:juejin.cn/post/7270173192789737487
收起阅读 »

趣解设计模式之《小王看病记》

〇、小故事 小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。 小王在项目期间也经常因为饮食不规范而导致胃疼,最近也...
继续阅读 »

〇、小故事


小王最近参与了公司一个大项目,工作很忙,经常加班熬夜,满负荷工作了2个月后,项目终于如期上线,并且客户反馈也特别的好。老板很开心,知道大家为这个项目付出了很多,所以给全组同事都放了1个星期的假。


小王在项目期间也经常因为饮食不规范而导致胃疼,最近也越来越严重了。所以他就想趁着这个假期时间去医院检查一下身体


他来到医院的挂号处,首先缴费挂号,挂了一个检查胃部的诊室。



小王按照挂号信息,来到了诊室,医生简单的询问了一下他的病情,然后给他开了几个需要检查的单子



小王带着医生开具的检查单,就在医院的收费处排队等待着缴费



缴费完毕后,小王就按照医生开的检查项目进行了身体检查……



那么从上面小王的一系列看病流程我们可以发现,这是一系列的处理过程,跟链条一样,即:



挂号——>开检查单——>缴费——>检查——>……



那么对于类似这种的业务逻辑,我们就可以使用一种设计模式来处理,即今天要介绍的——责任链模式


一、模式定义


责任链模式Chain of Responsibility Pattern



使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。



二、模式类图


下面我们再举一个例子,一家公司收到了好多的电子邮件,其中大致分为四类:



CEO处理】公司粉丝发来的邮件。

法律部门处理诽谤公司产品的邮件。

业务部门处理】要求业务合作的邮件。

直接丢弃】其他垃圾邮件。



这里需要CEO先查阅邮件处理,然后再由法务部处理,随后是业务部处理,最后是垃圾邮件执行废弃。根据以上的描述,我们首先需要邮件实体类Email,和用于区分不同处理方式的邮件类型EmailType。对于所有处理者,我们首先创建一个抽象的处理器类AbstractProcessor,再创建四个处理器的实现类,分别是CEO处理器CeoProcessor法务部门处理器LawProcessor业务部门处理器BusinessProcessor垃圾邮件处理器GarbageProcessor。具体类关系如下图所示:



三、模式实现


创建邮件实体类Email.java


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Email {
// 邮件类型
private int type;

// 邮件内容
private String content;
}

创建邮件类型枚举类EmailType.java


public enum EmailType {
FANS_EMAIL(1, "粉丝邮件"),
SLANDER_EMAIL(2, "诽谤邮件"),
COOPERATE_EMAIL(3, "业务合作邮件"),
GARBAGE_EMAIL(99, "垃圾邮件");

public int type;

public String remark;

EmailType(int type, String remark) {
this.type = type;
this.remark = remark;
}
}

创建抽象处理类AbstractProcessor.java


public abstract class AbstractProcessor {

// 责任链中下一个处理节点
private AbstractProcessor nextProcessor;

// 返回的处理结果
private String result;

public final String handleMessage(List emails) {
List filterEmails =
emails.stream().filter(email -> email.getType() == this.emailType()).collect(Collectors.toList());
result = this.execute(filterEmails);
if (this.nextProcessor == null) {
return result;
}
return this.nextProcessor.handleMessage(emails);
}

// 设置责任链的下一个处理器
public void setNextProcessor(AbstractProcessor processor) {
this.nextProcessor = processor;
}

// 获得当前Processor可以处理的邮件类型
protected abstract int emailType();

// 具体处理方法
protected abstract String execute(List emails);
}

创建CEO处理类CeoProcessor.java


public class CeoProcessor extends AbstractProcessor {
@Override
protected int emailType() {
return EmailType.FANS_EMAIL.type; // 处理粉丝来的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------CEO开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建法律部门处理类LawProcessor.java


public class LawProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.SLANDER_EMAIL.type; // 处理诽谤类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------法律部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建业务部门处理类BusinessProcessor.java


public class BusinessProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.COOPERATE_EMAIL.type; // 处理合作类型的邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------业务部门开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建垃圾邮件处理类GarbageProcessor.java


public class GarbageProcessor extends AbstractProcessor {

@Override
protected int emailType() {
return EmailType.GARBAGE_EMAIL.type; // 处理垃圾类型邮件
}

@Override
protected String execute(List emails) {
if (CollectionUtils.isNotEmpty(emails)) {
System.out.println("-------垃圾开始处理邮件-------");
emails.stream().forEach(email ->
System.out.println(email.getContent()));
}
return "任务执行完毕!";
}
}

创建责任链模式测试类ChainTest.java


public class ChainTest {
// 初始化待处理邮件
private static List emails = Lists.newArrayList(
new Email(EmailType.FANS_EMAIL.type, "我是粉丝A"),
new Email(EmailType.COOPERATE_EMAIL.type, "我要找你们合作"),
new Email(EmailType.GARBAGE_EMAIL.type, "我是垃圾邮件"),
new Email(EmailType.FANS_EMAIL.type, "我是粉丝B"));

public static void main(String[] args) {
// 初始化处理类
AbstractProcessor ceoProcessor = new CeoProcessor();
AbstractProcessor lawProcessor = new LawProcessor();
AbstractProcessor businessProcessor = new BusinessProcessor();
AbstractProcessor garbageProcessor = new GarbageProcessor();

// 设置责任链条
ceoProcessor.setNextProcessor(lawProcessor);
lawProcessor.setNextProcessor(businessProcessor);
businessProcessor.setNextProcessor(garbageProcessor);

// 开始处理邮件
ceoProcessor.handleMessage(emails);
}
}

执行后的结果


-------CEO开始处理邮件-------
我是粉丝A
我是粉丝B
-------业务部门开始处理邮件-------
我要找你们合作
-------垃圾开始处理邮件-------
我是垃圾邮件

Process finished with exit code 0

作者:爪哇缪斯
来源:juejin.cn/post/7277801611996676157
收起阅读 »

微博图床挂了!

一直担心的事情还是发生了。 作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然...
继续阅读 »

一直担心的事情还是发生了。


作为hexo多年的使用者,微博图床一直是我的默认选项,hexo+typora+iPic更是我这几年写文章的黄金组合。而图床中,新浪图床一直都是我的默认选项,速度快、稳定同时支持大图片批量上传更是让其成为了众多图床工具的默认选项。虽然今年早些的时候,部分如「ws1、ws2……」的域名就已经无法使用了,但通过某些手段还是可以让其存活的,而最近,所有调用的微博图床图片都无法加载并提示“403 Forbidden”了。




💡Tips:图片中出现的Tengine是淘宝在Nginx的基础上修改后开源的一款Web服务器,基本上,Tengine可以被看作一个更好的Nginx,或者是Nginx的超集,详情可参考👉淘宝Web服务器Tengine正式开源 - The Tengine Web Server



刚得知这个消息的时候,我的第一想法其实是非常生气的,毕竟自己这几年上千张图片都是用的微博图床,如今还没备份就被403了,可仔细一想,说到底还是把东西交在别人手里的下场,微博又不是慈善企业,也要控制成本,一直睁一只眼闭一只眼让大家免费用就算了,出了问题还是不太好怪到微博上来的。


那么有什么比较好的办法解决这个问题呢?


查遍了网上一堆复制/粘贴出来的文章,不是开启反向代理就是更改请求头,真正愿意从根本上解决问题的没几个。


如果不想将自己沉淀的博客、文章托管在印象笔记、notion、语雀这些在线平台的话,想要彻底解决这个问题最好的方式是:自建图床!


为了更好的解决问题,我们先弄明白,403是什么,以及我们存在微博上的图片究竟是如何被403的。


403


百度百科,对于403错误的解释很简单



403错误是一种在网站访问过程中,常见的错误提示,表示资源不可用。服务器理解客户的请求,但拒绝处理它,通常由于服务器上文件或目录的权限设置导致的WEB访问错误。



所以说到底是因为访问者无权访问服务器端所提供的资源。而微博图床出现403的原因主要在于微博开启了防盗链。


防盗链的原理很简单,站点在得知有请求时,会先判断请求头中的信息,如果请求头中有Referer信息,然后根据自己的规则来判断Referer头信息是否符合要求,Referer 信息是请求该图片的来源地址。


如果盗用网站是 https 的 协议,而图片链接是 http 的话,则从 https 向 http 发起的请求会因为安全性的规定,而不带 referer,从而实现防盗链的绕过。官方输出图片的时候,判断了来源(Referer),就是从哪个网站访问这个图片,如果是你的网站去加载这个图片,那么 Referer 就是你的网站地址;你的网址肯定没在官方的白名单内,(当然作为可操作性极强的浏览器来说 referer 是完全可以伪造一个官方的 URL 这样也也就也可以饶过限制🚫)所以就看不到图片了。



解决问题


解释完原理之后我们发现,其实只要想办法在自己的个人站点中设置好referer就可以解决这个问题,但说到底也只是治标不治本,真正解决这个问题就是想办法将图片迁移到自己的个人图床上。


现在的图床工具很多,iPic、uPic、PicGo等一堆工具既免费又开源,问题在于选择什么云存储服务作为自己的图床以及如何替换自己这上千张图片。



  1. 选择什么云存储服务

  2. 如何替换上千张图片


什么是OSS以及如何选择


「OSS」的英文全称是Object Storage Service,翻译成中文就是「对象存储服务」,官方一点解释就是对象存储是一种使用HTTP API存储和检索非结构化数据和元数据对象的工具。


白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传等一列服务,这样的服务以及技术可以统称为OSS,业内提供OSS服务的厂商很多,知名常用且成规模的有阿里云、腾讯云、百度云、七牛云、又拍云等。


对于我们这些个人用户来说,这些云厂商提供的服务都是足够使用的,我们所要关心的便是成本💰。


笔者使用的是七牛云,它提供了10G的免费存储,基本已经够用了。


有人会考虑将GitHub/Gitee作为图床,并且这样的文章在中文互联网里广泛流传,因为很多人的个人站点都是托管在GitHub Pages上的,但是个人建议是不要这么做。


首先GitHub在国内的访问就很受限,很多场景都需要科学上网才能获得完整的浏览体验。再加上GitHub官方也不推荐将Git仓库存储大文件,GitHub建议仓库保持较小,理想情况下小于 1 GB,强烈建议小于 5 GB。


如何替换上千张图片


替换文章中的图片链接和“把大象放进冰箱里”步骤是差不多的



  1. 下载所有的微博图床的图片

  2. 上传所有的图片到自己的图床(xx云)

  3. 对文本文件执行replaceAll操作


考虑到我们需要迁移的文件数量较多,手动操作肯定是不太可行的,因此我们可以采用代码的方式写一个脚本完成上述操作。考虑到自己已经是一个成熟的Java工程师了,这个功能就干脆用Java写了。


为了减少代码量,精简代码结构,我这里引入了几个第三方库,当然不引入也行,如果不引入有一些繁琐而又简单的业务逻辑需要自己实现,有点浪费时间了。


整个脚本逻辑非常简单,流程如下:



获取博客文件夹下的Markdown文件


这里我们直接使用hutool这个三方库,它内置了很多非常实用的工具类,获取所有markdown文件也变得非常容易


/**
* 筛选出所有的markdown文件
*/

public static List<File> listAllMDFile() {
List<File> files = FileUtil.loopFiles(VAULT_PATH);
return files.stream()
.filter(Objects::nonNull)
.filter(File::isFile)
.filter(file -> StringUtils.endsWith(file.getName(), ".md"))
.collect(Collectors.toList());
}

获取文件中的所有包含微博图床的域名


通过Hutools内置的FileReader我们可以直接读取markdown文件的内容,因此我们只需要解析出文章里包含微博图床的链接即可。我们可以借助正则表达式快速获取一段文本内容里的所有url,然后做一下filter即可。


/**
* 获取一段文本内容里的所有url
*
* @param content 文本内容
* @return 所有的url
*/

public static List<String> getAllUrlsFromContent(String content) {
List<String> urls = new ArrayList<>();
Pattern pattern = Pattern.compile(
"\\b(((ht|f)tp(s?)\\:\\/\\/|~\\/|\\/)|www.)" + "(\\w+:\\w+@)?(([-\\w]+\\.)+(com|org|net|gov"
+ "|mil|biz|info|mobi|name|aero|jobs|museum" + "|travel|[a-z]{2}))(:[\\d]{1,5})?"
+ "(((\\/([-\\w~!$+|.,=]|%[a-f\\d]{2})+)+|\\/)+|\\?|#)?" + "((\\?([-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)" + "(&(?:[-\\w~!$+|.,*:]|%[a-f\\d{2}])+=?"
+ "([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)*)*" + "(#([-\\w~!$+|.,*:=]|%[a-f\\d]{2})*)?\\b");
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
urls.add(matcher.group());
}
return urls;
}

下载图片


用Java下载文件的代码在互联网上属实是重复率最高的一批检索内容了,这里就直接贴出代码了。


public static void download(String urlString, String fileName) throws IOException {
File file = new File(fileName);
if (file.exists()) {
return;
}
URL url = null;
OutputStream os = null;
InputStream is = null;
try {
url = new URL(urlString);
URLConnection con = url.openConnection();
// 输入流
is = con.getInputStream();
// 1K的数据缓冲
byte[] bs = new byte[1024];
// 读取到的数据长度
int len;
// 输出的文件流
os = Files.newOutputStream(Paths.get(fileName));
// 开始读取
while ((len = is.read(bs)) != -1) {
os.write(bs, 0, len);
}
} finally {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
}
}

上传图片


下载完图片后我们便要着手将下载下来的图片上传至我们自己的云存储服务了,这里直接给出七牛云上传图片的文档链接了,文档里写的非常详细,我就不赘述了👇


Java SDK_SDK 下载_对象存储 - 七牛开发者中心


全局处理


通过阅读代码的细节,我们可以发现,我们的方法粒度是单文件的,但事实上,我们可以先将所有的文件遍历一遍,统一进行图片的下载、上传与替换,这样可以节约点时间。


统一替换的逻辑也很简单,我们申明一个全局Map,


private static final Map<String, String> URL_MAP = Maps.newHashMap();

其中,key是旧的新浪图床的链接,value是新的自定义图床的链接。


我们将listAllMDFile这一步中所获取到的所有文件里的所有链接保存于此,下载时只需遍历这个Map的key即可获取到需要下载的图片链接。然后将上传后得到的新链接作为value存在到该Map中即可。


全文替换链接并更新文件


有了上述这些处理步骤,接下来一步就变的异常简单,只需要遍历每个文件,将匹配到全局Map中key的链接替换成Map中的value即可。


/**
* 替换所有的图片链接
*/

private static String replaceUrl(String content, Map<String, String> urlMap) {
for (Map.Entry<String, String> entry : urlMap.entrySet()) {
String oldUrl = entry.getKey();
String newUrl = entry.getValue();
if (StringUtils.isBlank(newUrl)) {
continue;
}
content = RegExUtils.replaceAll(content, oldUrl, newUrl);
}
return content;
}

我们借助commons-lang实现字符串匹配替换,借助Hutools实现文件的读取和写入。


files.forEach(file -> {
try {
FileReader fileReader = new FileReader(file.getPath());
String content = fileReader.readString();
String replaceContent = replaceUrl(content, URL_MAP);
FileWriter writer = new FileWriter(file.getPath());
writer.write(replaceContent);
} catch (Throwable e) {
log.error("write file error, errorMsg:{}", e.getMessage());
}
});

为了安全起见,最好把文件放在新的目录中,不要直接替换掉原来的文件,否则程序出现意外就麻烦了。


接下来我们只需要运行程序,静待备份结果跑完即可。


以上就是本文的全部内容了,希望对你有所帮助


作者:插猹的闰土
来源:juejin.cn/post/7189651446306963514
收起阅读 »

🐞 如何成为一名合格的“中级开发”

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。 这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。 如果你是第一次看这个系列,我强烈建...
继续阅读 »

嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


在这个系列里面的上一篇文章中,我跟大家分享了怎么做一个专业的开发者,还有工作中要注意什么事情。


这是我们人生很重要的一步,因为只有学会怎么开始,才能慢慢变优秀,才能一步步往上进步。


如果你是第一次看这个系列,我强烈建议你回去看看我之前写的两篇文章,说不定能对你有帮助。


其实我想写这篇文章已经很久了,可是一直想不出来怎么写,找了很多资料也没用。


确实憋不出来,中间还水了一篇“JavaScript冷饭”文章。天可是天天炒冷饭不好吃啊,写那些水文总会心生愧疚,感觉对不起你们哈哈。


今天,我们继续聊一聊,当我们进入这个角色一两年后,该怎么摆脱“初级”头衔,迈入“中级”阶段呢?😎



注意事项:


我接下来提及的内容可能很多大佬跟我的意见是不同的。


也有可能我的知识有限,我只涵盖了前端开发工程师的部分,对其他岗位的开发工程师不了解,可能我说的指标并不一定能和贵公司考核时所授予给的职称相对应。


我这里说的是衡量开发人员技能、知识和整体能力的一般指标


它会根据所在的领域而变化,比如前端、后端、数据等等都不太一样。


虽然具体的工具、技术甚至架构知识可能有所不同,但是我说的一般原则应该是可以广泛适用的。


如果觉得我说错了,请在评论区交流。😊



🎖️ 中级开发的显著特点:“骄傲”


当你到了中级水平,你心里一定有一个想法。那就是:


我已经学会了我现在做的事情,以及要用的所有东西了!


再说得清楚一点就是:


“我已经完全会用JavaScript了,我对HTML很熟悉了,我对数据库没问题!”


“我已经完全会用Vue了,我也会用Angular开发”


这个时候的“中级开发”,觉得他已经有了这个领域需要的能力了。



我肯定每个人到了中级阶段后肯定会有这种感觉。


可能你觉得我要说的是开玩笑,但是大部分的“中级开发”肯定都经历过这个事情。



当然啦,我想表达的“骄傲”不是贬义词。


因为这个阶段只是我们成长中必须经历的一个阶段。这真的不是一件坏事。


“骄傲”不是一件坏事


我们小时候我们都会觉得,爸爸妈妈什么都不知道,我们才更明白


类似的,当你真正进入进入“中级开发“这个角色,你大概率的就会产生这类“骄傲的情绪”。


当你拥有“骄傲”,你才开始真正走自己的路。这个时候你才真正开始独立思考。


这意味着你已经积累了足够的知识和经验,可以继续精进设计模式、最佳实践等这些学科以拔高你的知识。


简单的东西已经不能吸引你了。


🚩 中级开发应该掌握什么?


现在你是中级开发了,你需要看看自己是不是能做到下面这些事情。


这些“新”的东西可以让中级开发更有经验,也更能帮助团队。


编程能力:



  1. 很清楚不同的系统(API、模块、包等)怎么互相连接

  2. 熟练使用编程工具(IDE、GIT等)

  3. 知道怎么实现一般的需求

  4. 遇到bug的时候,知道从哪里找原因和解决办法

  5. 知道怎么优化代码和重构代码

  6. 知道怎么提高性能

  7. 知道怎么用面向对象的程序设计

  8. 知道常用的软件架构模式(MVC、MVVM、MVP、MVI等)

  9. 知道编程语言的一些特点(函数式编程)

  10. 知道怎么部署系统应用

  11. 知道怎么用数据库索引

  12. 知道怎么用数据库表迁移

  13. 知道怎么用数据库分片技术


社会能力:



  1. 可以偶尔跟产品经理(客户)沟通

  2. 是团队的主力


开始优雅:



  1. 代码模块开始按照设计模式来写

  2. 对烂代码有敏感度和重构能力


等等


📌 对中级开发的一些建议


也许现在在读文章的你已经是一位中级开发的存在了,我现在有一些建议想要分享给你!


找一个自己感兴趣的开发者社区加入


为什么我们常说“好的团队创造个人”呢


因为当你真的参与到了重要或高价值的项目时,你真的比一个人漫无目的地学习更快地获得经验。


而且当你真正在团队中贡献力量地时候,你地团队,你的组长,你的领导都会知道,把事情交给你,你就能把自己做好。


在这个过程中,你能积累经验并在你的团队中声名鹊起(这不是名气,而是知名度),那么当新的机会出现时,你就能很快地把握住。


跳出舒适区


跟我上一篇提到的给初级开发的建议类似,你一定要经常的跳出自己的舒适区,不然你不会有毅力坚持学习。


而且,特别是在互联网行业,学习能力是个硬性指标,如果无法坚持下去,很容易就会被淘汰。


这样做可以开阔你的眼界,让你的知识面更广。最终,你会逐渐掌握开发的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


但是只要你坚持下去,未来的你一定会与其他人拉开差距。


找到你的导师


这一点在上一篇我也强调过了。你的开发生涯,不能只靠你自己摸索。


你需要有人给你提供想法并能够从中学习。特别是在“中级开发”阶段。


导师可以帮助你不会在某些技术问题或者人生问题上钻牛角尖,他可以拉你一把,避免你浪费很多时间。


这个人可以是你团队中的某个人。


也可以是网络上开发者社区中认识的某位博主。


找到你信任的人(或者更可能是一群人),你可以跟他们问问题和说想法!


找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


持续学习


这个没什么好说的,在这内卷的社会中,如果没有润的资本和能力,不如在持续学习中等待破局的机会!




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?当你处于这个阶段时,你发现什么对你帮助最大?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
来源:juejin.cn/post/7243203041872412731
收起阅读 »

实现滚动点赞墙

web
需要实现的效果如下: 需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~ 纯css实现 scss如下:(如果要将scss改为less,将$改为@就可以了) 当移动到第8行结束的时候,同屏出现的两行(第9行和第1...
继续阅读 »

需要实现的效果如下:



需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~


纯css实现



scss如下:(如果要将scss改为less,将$改为@就可以了)


当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了


这是一个上移的动画,动画执行的时间就是8s


itemShowTime+(itemShowTime + (oneCycleItemNum - oneScreenItemNum)(oneScreenItemNum) * (itemShowTime / $oneScreenItemNum)


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 2s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 8; // 单个循环上移的item条数
$oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

简单的demo:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

setInterval监听


css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准




import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)


export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index

useEffect(() => {
const timer = setInterval(() => {
replaceData()
},4900)

return () => {
clearInterval(timer)
}
}, [])


const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

IntersectionObserver监听


监听第5个元素


如果第五个元素可见了,意味着不可见时,需要更换数据了


如果第五个元素不可见了,立刻替换数据


替换的数据如下:



使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定


tsx如下:



import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'

const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
const ITEM_5_ID = 'item-5'

export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))

const intersectionObserverRef = useRef<IntersectionObserver | null>()
const item5Ref = useRef<HTMLDivElement | null>(null)

const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
const justVisible5 = useRef<boolean>(false) // 原来是否为可视

useEffect(() => {
intersectionObserverRef.current = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.target.id === ITEM_5_ID) {
// 与视图相交(开始出现)
if (item.isIntersecting) {
justVisible5.current = true
}
// 从可视变为不可视
else if (justVisible5.current) {
replaceData()
justVisible5.current = false
}
}
})
})
startObserver()

return () => {
intersectionObserverRef.current?.disconnect()
intersectionObserverRef.current = null
}
}, [])

const startObserver = () => {
if (item5Ref.current) {
// 对第五个 item 进行监测
intersectionObserverRef.current?.observe(item5Ref.current)
}
}

const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}

return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
index === 4 ?
<div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
:
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>

)
}

scss样式


$itemHeight: 60px; // 单个item的高度

$itemShowTime: 3s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 5; // 单个循环上移的item条数
$oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)

$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));

@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;

animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}

.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;

}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

作者:0522Skylar
来源:juejin.cn/post/7278244755825442853
收起阅读 »

用了策略模式之后,再也不用写那么多 if else 了,真香!

web
前言 从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式。 策略模式的定义 先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起...
继续阅读 »

前言


从我个人理解来看,设计模式其实就藏在我们平时的代码中,只是有人把它们提、炼出来,赋予了一些专业的名词和定义,下面给大家介绍一个日常项目开发中非常实用的设计模式,也就是策略模式


策略模式的定义


先来看下策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。


简单来说就是有多种选择,然后一般只会选择一种。从代码的角度来说就是,定义一系列的ifelseif,然后只会命中其中一个。


举个例子


话不多说,直接来看例子,比如我们需要计算员工工资,员工工资计算规则如下:



  • 高级工:时薪为25块/小时

  • 中级工:时薪为20块/小时

  • 初级工:时薪为15块/小时


按每天10小时的工作时长来算。


一、第一版实现:


const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workHours * 25
}
if (workerLevel === 'middle') {
return workHours * 20
}
if (workerLevel === 'low') {
return workHours * 15
}
}
console.log(calculateSalary('high')) // 250
console.log(calculateSalary('middle')) // 200

这段代码具有明显的缺点:



  • calculateSalary函数庞大,有许多的if else语句,这些语言需要覆盖所有的逻辑分支

  • calculateSalary函数缺乏弹性,如果新增一种员工等级higher,需要修改calculateSalary函数的内部实现,违反开放——封闭原则

  • 算法的复用性差


二、第二版实现(函数组合):


当然,我们可以使用函数组合的方式重构代码,把每一个if中的逻辑单独抽离成一个函数。


const workerLevelHigh = function (workHours) {
return workHours * 25
}

const workerLevelMiddle = function (workHours) {
return workHours * 20
}
const workerLevelLow = function (workHours) {
return workHours * 15
}

const calculateSalary = function (workerLevel, workHours = 10) {
if (workerLevel === 'high') {
return workerLevelHigh(workHours)
}
if (workerLevel === 'middle') {
return workerLevelMiddle(workHours)
}
if (workerLevel === 'low') {
return workerLevelLow(workHours)
}
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

这样会提高算法的复用性,但这种改善十分有限,calculateSalary函数依旧庞大和缺乏弹性。


三、第三版实现(策略模式):


我们可以把不变的部分和变化的部分拆分开来。



  • 不变的部分:算法的使用方式不变,都是根据某个算法取得计算后的工资数额;

  • 变化的部分:算法的实现。


我们js的对象是key value的形式,这可以帮助我们天然的替换掉if else


因此,我们可以定义对象的两部分:



  • 针对变化的部分,我们可以定义一个策略对象,它封装了具体的算法,负责具体的计算过程

  • 针对不变的部分,我们提供一个Context函数,它接受客户的请求,随后把请求委托给策略对象。


const strategies = {
"high": function (workHours) {
return workHours * 25
},
"middle": function (workHours) {
return workHours * 20
},
"low": function (workHours) {
return workHours * 15
},
}

const calculateSalary = function (workerLevel, workHours) {
return strategies[workerLevel](workHours)
}
console.log(calculateSalary('high', 10)) // 250
console.log(calculateSalary('middle', 10)) // 200

策略模式的优缺点


从我个人在实际项目中的使用来看,策略模式的优缺点如下:


优点:



  • 代码复杂度降低:再也不用写那么多if else了。eslint其中有一项规则配置叫圈复杂度,其中一条分支也就是一个if会让圈复杂增加1,圈复杂度高的代码不易阅读和维护,用策略模式就能很好的解决这个问题;

  • 易于切换、理解和扩展:它将算法封装在独立的strategy中,比如你要在上面代码中加一个等级higherlower,直接更改策略对象strategies就行,十分方便。

  • 复用性高:策略模式中的算法可以复用在系统的其它地方,你只需要用将策略类strategies用export或者module.exports导出,就能在其他地方很方便的复用。


缺点:



  • 增加使用者使用负担:因为大量运用策略模式会在实际项目中堆砌很多策略类或者策略对象,这样项目的新人如果不熟悉这些策略类和策略对象,会增加他们的使用成本和学习成本,前期来说会比看if else更加难懂。


小结


以上就是我个人对策略模式的解读和了解啦,实际上项目中用策略模式的场景还是挺多的,因为在写业务代码中,很容易写出大量的if else,这时候就可以封装为策略模式,方便项目维护和扩展,从我个人的使用体验来看,还是相当香的。


大家喜欢在实际项目使用策略模式么,欢迎留言和讨论~


作者:han_
来源:juejin.cn/post/7279041076273610764
收起阅读 »

27岁程序媛未来的出路到底在哪里?

不太聪明的脑子的思考原因 最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养, 看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安, 不过作为i型...
继续阅读 »

不太聪明的脑子的思考原因


最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养,

看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安,

不过作为i型人格真的很喜欢这种沉浸式工作,暂时没有换行业的打算,所以还是先从目前做程序出发,去提升自己的能力,争取能再多干个几年,然后回东北老家花几万块买个小房子,开始我的摆烂养老人生
(人生终极目标)


在此总结一下今年上半年的成果和下半年的目标吧~


上半年成果


1.刷力扣拿到排名


摆烂人生是在去年感知到危机的时候结束的,于是开始疯狂刷LeetCode,学习算法,最终的结果是对待代码问题脑子变得灵光了但生活中越发糊涂了,但是目前困难的题还是基本摸不到头绪的状态,好多数学公式也不知道,位运算符也不咋会用,就目前感觉自己还是很差,提升的空间还是非常非常高的

(今年四月拿到排名时截的图)


微信图片_20230823180111.jpg

2.开始准备软考


年初的时候开始考虑考一个专业资格证,于是开始做一些功课,上半年从bilibili上看了一些公开的课先做了初步了解,六月份买了一套课开始进行系统的学习,备战11月的考试


微信图片_20230823180721.png
微信图片_20230823180710.png

3.涨薪


很幸运自己能在目前经济环境下行的情况下没有失业,并且领导对我还算认可,给我们在竞争中留下来的人涨了工资,但说是涨薪,其实最终结果我们未必拿到的多了,因为目前公司效益不景气,如果公司效益持续低迷,年底的14薪必定要打水漂,但是还能稳定的存活下来也算是比较满意了,真心希望公司越来越好,因为我们的老板人真的非常不错(虽然我不接受pua但是发自内心感谢公司)


4.买了自行车开始骑行健身


其实早就想买个自行车,可以骑行上班,周末也可以当运动,不过身边的好多人都不赞同,因为像夏天太热、冬天太冷、刮风下雨都骑不出去,但是最终我还是买了,嘎嘎开心,不过确实影响因素很多最终也没骑过几次哈哈(主要是本人太懒总是找借口不骑车出门)


微信图片_20230823181638.jpg

下半年目标


1.软考通过!


最近还是按照规划的持续学习,每个月给自己定一个总体的目标,然后分到每一天里去,现在距离考试还有两个多月,还是要加油的!


2.争取换一个更高的平台


感觉目前的公司体量还是太小了,做了很多微信小程序,工作对自己的提升已经到达了极限,但是就目前的情况来说,还是对年底的14薪抱有一丝丝幻想,所以这个目标可能在今年年底或者明年去达成


3.持续精进算法


还是在有条不紊的刷LeetCode,给自己的最低要求是每周至少一道中级,保持一个持续学习的状态


4.做一个开源项目


这个规划应该会在11月份开始实施,或者如果突然来了灵感可以立马启动,也是给以后面试提供一个优势条件吧


最后希望还在圈子中的同行们也能越来越好,不管这些努力会不会给自己带来实质性的收益,本质上都是在提升自己,目的其实很简单,就是不被这日新月异的时代所淘汰,
就好像一句金句里描述的那样:我们所做的一切,不是为了改变世界,而是不让世界改变我们!



最后,大家有什么能提升自己的点子也可以给我留言,让我们一起努力吧,加油!


2648ff5ff16d83fcde8e1f6117c4f472.jpeg


作者:毛毛裤
来源:juejin.cn/post/7270403438201356346
收起阅读 »

回乡偶书

离别家乡岁月多,近来人事半消磨。 惟有门前镜湖水,春风不改旧时波。 1200多年前,耄耋诗人告老,从北方回他萧绍一带的家乡。多年后,而立之年的晚辈从萧山出发,一夜辗转赶回北方老家,需要在窄窄几天假期里,拜堂成亲洞房花烛。随后便要返程,收敛和压抑个性,作为被标准...
继续阅读 »

离别家乡岁月多,近来人事半消磨。
惟有门前镜湖水,春风不改旧时波。


1200多年前,耄耋诗人告老,从北方回他萧绍一带的家乡。多年后,而立之年的晚辈从萧山出发,一夜辗转赶回北方老家,需要在窄窄几天假期里,拜堂成亲洞房花烛。随后便要返程,收敛和压抑个性,作为被标准化了的打工人,回归互联网行业最知名几家工厂流水线生产工的身份。


7年前茕茕一人离开时,心意诀决,并无此刻留恋情愫。我想离开熟悉而平平无奇的这里,想在发达的大城市闯荡立足。几年艰苦卓绝,起起伏伏壮阔波澜,如愿在都市有一席之地。当7年后匆匆返乡,成婚祭祖,再要和新妻携手南下时,更多却是不舍。


这是新的开始,但何尝不是结束?告别从小见我长大的故交亲友,告别家乡,人生前几十年的社交圈若即若离渐行渐远。而要去遥远的南方,认识新的朋友,开辟新的章节。


见到了眼熟却不能明确是何亲戚,更不知名姓住址的宾客,当年的中年人苍苍白发垂垂老矣,带着相见不相识的儿童——我更清楚与此同时,当年的许多老人,限于距离限于身体,甚至是生死阴阳,并不能见到。


”而这些复活的情愫仅仅只能引发怀旧的兴致,根本不能重新再去领受。恰如一只红冠如血尾翎如帜的公鸡发现了曾经哺育自己的那只蛋壳,却再也无法重新蜷卧其中体验那蛋壳里头的全部美妙了“(「白鹿原」白孝文回乡)。它要觅食要筑窝,要跳上垛头,引吭高歌。


“近来人事半消磨”,读过多次不能领会。当亲眼看到原本肥胖的人失去意识躺卧病榻,瘦削枯槁。更有比我还小的同村邻里,远房亲戚,二十二三,刚刚毕业,因意外或疾病戛然而止。


“惟有门前镜湖水,春风不改旧时波”。这是一声无可奈何的哀叹?还是看通之后的豁达?也或许兼而有之。作者被称有唐一代,福禄寿考典范。当身经目睹这些,也认同高龄高寿,没有痛苦或较短暂病痛后,在满堂子孙目送中离开,对当事人实在算不得坏事。


image.png


这是被繁忙和xx偷走的几年,大疫过后,这世界和之前已经很不一样了。


作者:fliter
来源:juejin.cn/post/7275592320010272805
收起阅读 »

git merge 和 git rebase的区别

git rebase 让你的提交记录更加清晰可读 git rebase 的使用 rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。 如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。 现在我们...
继续阅读 »

git rebase 让你的提交记录更加清晰可读


git rebase 的使用


rebase 翻译为变基,它的作用和 merge 很相似,用于把一个分支的修改合并到另外一个分支上。


如下图所示,下图介绍了经过 rebase 前后提交历史的变化情况。



现在我们来用一个例子来解释一下上面的过程。


假设我们现在有2条分支,一个为 master\color{#2196F3}{master} ,一个为 feature/1\color{#2196F3}{feature/1},他们都基于初始的一个提交 add readme\color{#2196F3}{add \ readme} 进行检出分支,之后,master 分支增加了 3.js\color{red}{3.js},和 4.js\color{red}{4.js} 的文件,分别进行了2次提交,feature/1\color{#2196F3}{feature/1} 也增加了 1.js\color{red}{1.js}2.js\color{red}{2.js} 的文件,分别对应以下2条提交记录。


master\color{#2196F3}{master} 分支如下图:



feature/1\color{#2196F3}{feature/1} 分支如下图:



结合起来看是这样的:



此时,切换到 feature/1 分支下,执行 git rebase master ,成功之后,通过 log 查看记录。


如下图所示:可以看到先是逐个应用了 master 分支的更改,然后以 master\color{#2196F3}{master} 分支最后的提交作为基点,再逐个应用 feature/1\color{#2196F3}{feature/1} 的每个更改。



所以,我们的提交记录就会非常清晰,没有分叉,上面演示的是比较顺利的情况,但是大部分情况下,rebase 的过程中会产生冲突的,此时,就需要手动解决冲突,然后使用 git addgit rebase --continue 的方式来处理冲突,完成 rebase,如果不想要某次 rebase 的结果,那么需要使用 git rebase --skip 来跳过这次 rebase


git merge 和 git rebase 的区别


不同于 git rebase的是,git merge 在不是 fast-forward(快速合并)的情况下,会产生一条额外的合并记录,类似 Merge branch 'xxx' into 'xxx' 的一条提交信息。



另外,在解决冲突的时候,用 merge 只需要解决一次冲突即可,简单粗暴,而用 rebase 的时候 ,需要一次又一次的解决冲突。


git rebase 交互模式


在开发中,常会遇到在一个分支上产生了很多的无效的提交,这种情况下使用 rebase 的交互式模式可以把已经发生的多次提交压缩成一次提交,得到了一个干净的提交历史,例如某个分支的提交历史情况如下:



进入交互式模式的方法是执行:


git rebase -i <base-commit>

参数 base-commit 就是指明操作的基点提交对象,基于这个基点进行 rebase 的操作,对于上述提交历史的例子,我们要把最后的一个提交对象 (ac18084\color{#F19E38}{ac18084}) 之前的提交压缩成一次提交,我们需要执行的命令格式是


git rebase -i ac18084

此时会进入一个 vim 的交互式页面,编辑器列出的信息像下列这样。



想要合并这一堆更改,我们要使用 squash 策略进行合并,即把当前的 commit 和它的上一个 commit 内容进行合并, 大概可以表示为下面这样。


pick  ... ...
s ... ...
s ... ...
s ... ...

修改文件后 按下 : 然后 wq 保存退出,此时又会弹出一个编辑页面,这个页面是用来编辑提交的信息,修改为 feat: 更正,最后保存一下,接着使用 git branch 查看提交的 commit 信息,rebase 后的提交记录如下图所示,是不是清爽了很多? rebase 操作可以让我们的提交历史变得更加清晰。




特别注意,只能在自己使用的 feature 分支上进行 rebase 操作,不允许在集成分支上进行 rebase,因为这种操作会修改集成分支的历史记录。



rebase 的风险



patch:【假设本地分支为 dev1,c1 和 c2 是本地往 dev1 分支上做的两次提交】把 dev1 分支上的c1和 c2 “拆”下来,并临时保存成 c1' 和 c2'。git 里将其称为 patch



rebase\color{red}{rebase} 会将当前分支的新提交拆下来,保存成 patch\color{red}{patch},然后合并进其他分支新的 commit\color{red}{commit},最后将 patch\color{red}{patch} 接进当前分支。这是 rebase\color{red}{rebase} 对多条分支的操作。对于单条分支,rebase\color{red}{rebase} 还能够合并多个 commit\color{red}{commit} 单号,将多个提交合并成一个提交。


git rebase -i [commit id]命令能够合并(整改) commit id 之前的所有 commit\color{red}{commit} 单。加上-i选项能够提供一个交互界面,分阶段修改commit信息并 rebase\color{red}{rebase}


但这里就会出现一个问题:如果你合并多个单号时,一不小心合并多了,将别人的提交也合并了,此时你本地的 commit history\color{red}{commit \ history} 和远程仓库的 commit history\color{red}{commit \ history} 不一样了,无论你如何 push\color{red}{push},都无法推送你的代码了。如果你并不记得 rebase\color{red}{rebase} 之前的 HEAD\color{red}{HEAD} 指向的 commit\color{red}{commit}commit ID\color{red}{commit \ ID} 的话,git reflog\color{red}{git \ reflog} 都救不了你。


tips:  你可以 push\color{red}{push} 时带上 f\color{red}{-f} 参数,强制覆盖远程 commit history\color{red}{commit \ history},你这样做估计会被打,因为覆盖之后,团队的其他人的本地 commit history\color{red}{commit \ history} 就与远程的不一样了,都无法推送了。


因此,请保证仅仅对自己私有的提交单进行 rebase\color{red}{rebase} 操作,对于已经合并进远程仓库的历史提交单,不要使用 rebase\color{red}{rebase} 操作合并 commit\color{red}{commit} 单。


作者:d_motivation
来源:juejin.cn/post/7277089907974357052
收起阅读 »

谈谈2年前非科班3年前端经验的我是如何进外企的

先放结论,主要靠下面几点 有进外企的意识,并提前准备 运气 一定的技术水平和从业背景 下面进入正题。 有进外企的意识,并提前准备 去之前的几年,我就在网上刷到过不少外企的分享,类似 朝9晚5,不加班 请假随时请,不需要理由 15 天年假 看病不花钱、有补...
继续阅读 »

先放结论,主要靠下面几点



  • 有进外企的意识,并提前准备

  • 运气

  • 一定的技术水平和从业背景


下面进入正题。


有进外企的意识,并提前准备


去之前的几年,我就在网上刷到过不少外企的分享,类似



  • 朝9晚5,不加班

  • 请假随时请,不需要理由

  • 15 天年假

  • 看病不花钱、有补充商业医疗险

  • 零食、水果,,,,


以及下面的缺点



  • 不容易升高管,天花板低

  • 薪资不太高


考虑到国内的情况,把外企作为 40 岁甚至之后的一个选择,似乎是个不错的方案。


于是,我做了一下粗略的分析,当我准备投外企的时候,我大概需要具备下面的能力



  • 英文读写流利

  • 口语流利最好,最起码要能听懂,说的清楚

  • 技术水平和工作经验要大致匹配


显然其中最大的问题是英文听说读写,于是我把英语融入开发中,刻意的用英文查、答问题,看英文文档,写英文博客,来提升英文水平,所以最终在读写方面提升不少,听说上面,似乎还行。虽然全英面的时候,听力上吃了不少亏。


所以,如果你有这个想法,但是实力还不够,要提前准备,现在英语不行,不代表1-2年后不行。


运气


当时我司的上海分公司还在大规模招人,我运气好,赶上了,换做 2023 年,连 HC 都没了。


但是不是今年的你因此就放弃这个选择了呢?如果你仍然考虑把外企作为一个选项的话,那我的建议是,长期关注,有机会就试试


在一段时间内,外企缩招是很正常的,但如果你周期拉的够长,一边上班,一边关注机会,总会看到机会的。不过,拿到正式 offer 前,要稳住


如果已经离职了,根据自己评估来,外企面试流程2个礼拜都是很正常的事情,提前规划,如果急着找工作的话,这个时候就真的是看运气了。


如果你能把自己安排在面试官差不多准备下决定的时候,轮到你面试,那也是可以增加录取的概率的,就是不太容易。


一定的技术和从业背景


面试总归还是要看从业背景和技术的,如果有外国面试官,而你的公司也在国外有名,那自然是加分项。


如果发现外企的技术栈和国内不太重合,也不要怕,有时候反而是机会。


如果看到技术栈不太符,而你又愿意进这家公司,这个时候你的竞争对手就少了很多。如果你再愿意去多了解一下,优势就更多了。


外企哪里找


我当时是照着 955.WLB 的公司名单,挨个去搜索、尝试的。


上面名单中的外企,我当时就面过 leetcode 和 我司,主要外企招 3 年经验前端的也不是很多,反倒是 5年经验 + Java 系全栈方向更受外企欢迎,机会多不少


上面名单中,不加班的公司还是有一些的。但是如果剔除公积金、社保避税的话(主要是国内公司),选择不多。


作者:xianshenglu
来源:juejin.cn/post/7271900840246689829
收起阅读 »

八百年不面试,一面试就面得一塌糊涂

web
前言 好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。 最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈 目前是三面,但是估计止步于三面了,然后我稍微整理...
继续阅读 »

前言


好久没面试了,最近看一些厂开始招人了,于是投了投,没想着过,主要是抱着学习的态度,看看自己哪里不足,没想到自己这么不足。。。


最近面了个试,某大厂,具体是哪个厂大家可以自行猜测,我看看有没有对的,哈哈哈哈


目前是三面,但是估计止步于三面了,然后我稍微整理了一下面试题,但是这里只说出我的思考,而不说出答案,至于为啥不写答案,我只能说,我自己不会。。。有些内容我需要进行学习,然后系统地、简单易懂地分享给大家


我说一下我自身的情况:主要技术栈是vue全家桶,算是能深挖的那种,其他的,react、webpack、vite、less、sass、tailwindcss、unocss、Nuxt、node等系列都会,但是说实话我是没法手写出来的,只停留在会用的程度,webgl、canvas等可视化方向还可以,毕竟我之前就是做这个的,算法还算可以,不说精通,但是一般题是可以做出来的,然后在基础方面,也就是js、css这块,我只能说了解吧,因为见过css大神coco这种的,就感觉自己css从熟悉变成了听说。


反正大概的就那样吧,会的比较多,比较杂,但是很多都不精通(不去看源码这种),关键的来了,我对于浏览器这块比较薄弱,计网、操作系统对我来说像噩梦一样,我觉得是我经验少吧,没能在工作中接触到这几个层面,所以我就是真的能答出来,也就是硬背的,并且不能举一反三,这也导致了这次面试的惨败



下面我说一下面试吧


注意,公司的技术栈主要是React(umi)这块,vue很少很少,然后会用node写一些中间件,大部分都是大前端



然后算法问题的话,也不在这里说,主要说一些口述的问题


一面


一面是对我来说最友好的一面了,基本上都是简单的一些基础问题


面试官主要是react技术栈,然后我和他说了,我主要是vue的,vue的原理可以,但是问我react的太深的问题我是不太会的,首先是自我介绍,然后开始问问题



  • pinia和vuex的区别,其实他想问我Redux和Mobx和其他React状态管理的区别,但是奈何我就会这几个,所以他索性问了问了我pinia

  • css实现DOM节点的水平居中有几种方式:我记得我说了四种,flex,text-align,margin,position,应该还有,但是一瞬间的话,脑袋瓦特了

  • 实现一个左右布局,左侧200px,右侧自适应,css写有几种方式:我说了浮动、定位、弹性盒、网格这四种

  • 检测js数据类型,typeof和instanceof区别,instanceof原理:这里我直接手写了instanceof,这个很简单

  • 浏览器输入url,到看到页面会发生什么:我当时懵了,我看过n个面经都说过这个问题,经典八股,但是我就是没背,只能磕磕巴巴说了一些(我八股真的不行,而且我不背这玩意)

  • 用Java的时候,对登录请求进行拦截,怎么处理的:这个很简单哈,为啥问Java,这是因为我简历上有,我之前从事过全栈,然后他就问了一下

  • 函数式编程的副作用是什么

  • 工作的经历,项目问题(这个占据了大部分的时间),其中有个问题可以分享一下,因为我用了wangeditor,他问我wangeditor的内核是什么


一面总体来说是很友好的,而且都答出来了,面试官很礼貌,面试感受非常好,第二天下午的时候通知二面


二面


噩梦的开始



  • 自我介绍

  • 公司项目问题(绝大部分时间)

  • vue、react数据绑定的区别

  • 我想存储一个客户端的数据,前端有哪些存储方式:后来就存储、内存的问题开始展开

  • pinia会进行数据的存储,它最终存在了哪里

  • js的内存是怎么进行管理的

  • 垃圾回收、内存泄漏,什么情况会导致内存泄漏

  • 闭包是什么,应用场景,怎么操作会产生内存泄漏

  • 你在工作时用的哪种协议

  • 除了http还有哪些通信协议(跟前端有关的)

  • websocket通信过程是怎么样的

  • 前端跨域相关问题

  • 代理相关问题

  • 服务和服务之间有没有跨域

  • 前端安全方面有哪些攻击方式

  • 该怎么处理呢

  • node有哪些框架可以处理脚本攻击(或者是库)


有些问题记不清了,后面有一些网络的问题,但是忘了,前面其实还好,而且问题是一步一步衍生出来的,这感觉很好,但是到网络安全这里,我就有点不会了,当时就感觉完犊子了,再见


然后过了三天,hr电话告诉我过了,约了三面,其实是比较吃惊的,我以为已经止步了


三面


最难受的一面



  • 自我介绍

  • 说说最近自己认为最好的项目,然后我说了一些,然后对方:就这?我一时语塞,开始紧张(项目占据了大多数时间)

  • 说说tcp三次挥手,为什么不能两次

  • tcp粘包,讲讲

  • 还有一些计网和操作系统的问题,这里是因为,我根本不会,所以压根没记住问题。。

  • 进程、线程区别,举个生动的例子

  • 讲讲多线程

  • 浏览器的核心线程和核心进程有哪些

  • MySQL的引擎

  • 现在有一个100tb的文件,让你一分钟之内把这个文件遍历出来,怎么做


计网和操作系统一塌糊涂,现在面试还没有反馈,凉凉了,而且看面试官的态度也能看出来是很不满意的


总结


平均时长在45min左右


几乎没问vue的任何问题,这是我最难受的,而且js、ts、css也几乎不问的,反正就是我上面的技术栈几乎一个没问,面试官主要就问你两处:你的工作经验(也就是你曾经的公司项目),以及计网和操作系统


因为我有做一些开源的项目和个人的项目,但是他们更在乎你之前公司的项目是什么样的


我自己的项目比较多,简历就有5.6页,但是没啥用,他们都没问


我也发现自己计网、操作系统这里太薄弱了,有时间还是得系统学习一下的,自己的确在开发中没遇到过这些,欠加思考


希望大家也能重视一下这里吧


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

第一次使用缓存,因为没预热,翻车了

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。 悲惨的上线时刻 事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状...
继续阅读 »

缓存不预热会怎么样?我帮大家淌了路。缓存不预热会导致系统接口性能下降,数据库压力增加,更重要的是导致我写了两天的复盘文档,在复盘会上被骂出了翔。


悲惨的上线时刻


事情发生在几年前,我刚毕业时,第一次使用缓存内心很激动。需求场景是虚拟商品页面需要向用户透出库存状态,提单时也需要校验库存状态是否可售卖。但是由于库存状态的计算包含较复杂的业务逻辑,耗时比较高,在500ms以上。如果要在商品页面透出库存状态那么商品页面耗时增加500ms,这几乎是无法忍受的事情。


如何实现呢?最合适的方案当然是缓存了,我当时设计的方案是如果缓存有库存状态直接读缓存,如果缓存查不到,则计算库存状态,然后加载进缓存,同时设定过期时间。何时写库存呢? 答案是过期后,cache miss时重新加载进缓存。 由于计算逻辑较复杂,库存扣减等用户写操作没有同步更新缓存,但是产品认可库存状态可以有几分钟的状态不一致。为什么呢?


因为仓库有冗余库存,就算库存状态不一致导致超卖,也能容忍。同时库存不足以后,需要运营补充库存,而补充库存的时间是肯定比较长的。虽然补充库存完成几分钟后,才变为可售卖的,产品也能接受。 梳理完缓存的读写方案,我就沉浸于学习Redis的过程。


第一次使用缓存,我把时间和精力都放在Redis存储结构,Redis命令,Redis为什么那么快等方面的关注。如饥似渴的学习Redis知识。


直到上线阶段我也没有意识到系统设计的缺陷。


代码写的很快,测试验证也没有问题。然而上线过程中,就开始噼里啪啦的报警,开始我并没有想到报警这事和我有关。直到有人问我,“XXX,你是不是在上线库存状态的需求?”。


我人麻了,”怎么了,啥事”,我颤抖的问


“商品页面耗时暴涨,赶紧回滚”。一个声音传来


“我草”,那一瞬间,我的血压上涌,手心发痒,心跳加速,头皮发麻,颤抖的手不知道怎么在发布系统点回滚,“我没回滚过啊,咋回滚啊?”


“有降级开关吗”? 一个声音传来。


"没写..."。我回答的时候觉得自己真是二笔,为啥没加降级啊。(这也是复盘被骂的重要原因)


那么如何对缓存进行预热呢?


如何预热缓存


灰度放量


灰度放量实际上并不是缓存预热的办法,但是确实能避免缓存雪崩的问题。例如这个需求场景中,如果我没有放开全量数据,而是选择放量1%的流量。这样系统的性能不会有较大的下降,并且逐步放量到100%。


虽然这个过程中,没有主动同步数据到缓存,但是通过控制放量的节奏,保证了初始化缓存过程中,不会出现较大的耗时波动。


例如新上线的缓存逻辑,可以考虑逐渐灰度放量。


扫描数据库刷缓存


如果缓存维度是商品维度或者用户维度,可以考虑扫描数据库,提前预热部分数据到缓存中。


开发成本较高。除了开发缓存部分的代码,还需要开发扫描全表的任务。为了控制缓存刷新的进度,还需要使用线程池增加并发,使用限流器限制并发。这个方案的开发成本较高。


通过数据平台刷缓存


这是比较好的方式,具体怎么实现呢?


数据平台如果支持将数据库离线数据同步到Hive,Hive数据同步到Kafka,我们就可以编写Hive SQL,建立ETL任务。把业务需要被刷新的数据同步到Kafka中,再消费Kafka,把数据写入到缓存中。在这个过程中通过数据平台控制并发度,通过Kafka 分片和消费线程并发度控制 缓存写入的速率。


这个方案开发逻辑包括ETL 任务,消费Kafka写入缓存。这两部分的开发工作量不大。并且相比扫描全表任务,ETL可以编写更加复杂的SQL,修改后立即上线,无需自己控制并发、控制限流。在多个方面ETL刷缓存效率更高。


但是这个方案需要公司级别支持 多个存储系统之间可以进行数据同步。例如mysql、kafka、hive等。


除了首次上线,是否还有其他场景需要预热缓存呢?


需要预热缓存的其他场景


如果Redis挂了,数据怎么办


刚才提到上线前,一定要进行缓存预热。还有一个场景:假设Redis挂了,怎么办?全量的缓存数据都没有了,全部请求同时打到数据库,怎么办。


除了首次上线需要预热缓存,实际上如果缓存数据丢失后,也需要预热缓存。所以预热缓存的任务一定要开发的,一方面是上线前预热缓存,同时也是为了保证缓存挂掉后,也能重新预热缓存。


假如有大量数据冷启动怎么办


假如促销场景,例如春节抢红包,平时非活跃用户会在某个时间点大量打开App,这也会导致大量cache miss,进而导致雪崩。 此时就需要提前预热缓存了。具体的办法,可以考虑使用ETL任务。离线加载大量数据到Kafka,然后再同步到缓存。


总结



  1. 一定要预热缓存,不然线上接口性能和数据库真的扛不住。

  2. 可以通过灰度放量,扫描全表、ETL数据同步等方式预热缓存

  3. Redis挂了,大量用户冷启动的促销场景等场景都需要提前预热缓存。


作者:他是程序员
来源:juejin.cn/post/7277461864349777972
收起阅读 »

王兴入局大模型!美团耗资21亿拿下光年之外100%股权

【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。**** 官宣了!美团以20.65亿人民币收购光年之外。 就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。 总代价包括现金233,673,600美元;债务承担人民币366,...
继续阅读 »
【新智元导读】 正式官宣!美团收购光年之外全部权益,斥资20.65亿。****

官宣了!美团以20.65亿人民币收购光年之外。


就在刚刚,美团在港交所公告,已订立交易协议收购光年之外的全部权益。


总代价包括现金233,673,600美元;债务承担人民币366,924,000元;现金人民币1.00元。




于公告日期,光年之外的净现金总额约为285,035,563美元。转让协议交割完成后,美团将持有光年之外100%权益。


前几天,光年之外联合创始人王慧文因健康问题暂时离岗引发许多人的关注。


甚至,外界关心诸多的是他的停职对公司造成哪些影响。




美团在公告中对于并购的解释是,通过收购事项获得领先的AGI技术及人才,有机会加强其于快速增长的人工智能行业中的竞争力。


这次,美团出手,意味着光年之外在后续运营有了足够资金支持。


同时,对美团来说,大模型能对未来业务转型也将产生有利帮助。


美团拿下光年之外



其实,在外界看来,美团收购光年之外,就像是板凳钉钉的事。


从感性层面讲,王兴与王慧文是清华的室友,在创业路上并肩作战。王慧文入局大模型后,王兴紧接着应声跟进。


在大模型爆火后,美团CEO王兴也对此表示极大的关注,甚至,在3月份还投资光年之外。


当时,王兴表示「AI大模型让我既兴奋于即将创造出来的巨大生产力,又忧虑它未来对整个世界的冲击。老王和在创业路上同行近二十年,既然他决心拥抱这次大浪潮,那我必须支持。」




从理性层面讲,自2019年美团将战略升级为「零售+科技」后,不论是王兴本人,还是公司来讲,对AI也投入非常大的兴趣。


当前,大模型已经成为兵家必争之地,国内许多头部科技纷纷入局。


据「豹变」独家报道,美团做大模型,已经有2个多月,几乎是与王兴投资光年之外同步进行的。


据称,算法团队正积极扩招,甚至还在筹划成立单独的「平台部门」,帮助美团大模型通过具体的商业化形式落地。


对美团来讲,智能配送系统、外卖无人车等场景,都需要AI驱动。


收购光年之外后,美团能够将大模型的能力,与自家核心业务相结合,比如外卖、本地生活服务等等。




此外,还能够在客服、物流、产品体验等各种场景中实现应用,将大模型能力与场景深度融合。


美团方面表示,并购完成后,将支持光年团队继续在大模型领域进行探索和研究。


所以说,美团的未来还是值得期待的。


而前几日,王慧文病倒的消息,让外界猜测纷纷,比如融资不顺利,或团队组建困难。


有国内媒体澄清道,光年之外A轮融资已经完成一个月,融资到账实际金额远高于外部报道的2.3亿美元,网传的“融资不顺利”消息,属于谣言。


此次美团在港交所的公告,也证实了这一点。


而在人才组队上,光年之外也进展顺利。


在成立后不久,光年之外就以换股形式收购了一流科技,原核心技术团队被保留。


而在两个月内,光年之外的研发团队规模就已经在70人左右,团队在算法等领域,研发经验丰富。


这样一支已经组建成熟的团队,在当下的大模型之战中,无疑属于稀缺资源。


美团选择收购光年之外,显然也是经过深思熟虑。


VC平稳退出



光年之外在6月初刚刚完成了的这笔2.3亿美元的融资,由源码资本领投。


腾讯、五源资本和快手创始人宿华也参与了这次融资。


从港交所披露的信息来看,除了6月初的这轮融资,红杉中国也在前期对光年之外进行了投资。


当王慧文因病离开光年之外的领导岗位之后,这些前期投资的VC都因为这突发的黑天鹅事件,可能面临着投资打水漂的风险。


但是随着美团的出手收购,这些参与光年之外的投资至少能在一定程度上落袋为安。


不用再担心因为被投公司核心创始人离职给被投公司带来的巨大影响。


王慧文辞任董事



此前,大模型创业4个月,王慧文就因身体原因停职休养。


紧接着,美团在港交所公布,王慧文已提出辞去公司非执行董事、公司董事会提名委员会成员和公司授权代表职务,自6月26日起生效。




在王慧文卸任董事后,美团宣布,执行董事穆荣均已获委任为授权代表,自2023年6月26日起生效。


另外,提名委员会将由冷雪松先生和沈向洋博士组成,冷雪松继续担任提名委员会主席。


在另一份公告中,美团更新了董事会成员。王兴和穆荣均担任公司执行董事,沈南鹏为非执行董事,欧高敦、冷雪松、沈向洋是独立非执行董事。



参考资料:


www1.hkexnews.hk/listedco/li…


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

月入5000+?Midjourney制作小红书壁纸实现副业变现

上一篇文章介绍了Midjourney实现副业变现的6种方式,也介绍了小红书壁纸的变现逻辑:第一种是开通店铺,吸引用户进行购买;第二种是引导用户在某个小程序下载图片,用户每下载一张,我们便会有0.3元左右的收益。这一篇开始介绍如何使用Midjourney制作小红...
继续阅读 »

上一篇文章介绍了Midjourney实现副业变现的6种方式,也介绍了小红书壁纸的变现逻辑:第一种是开通店铺,吸引用户进行购买;第二种是引导用户在某个小程序下载图片,用户每下载一张,我们便会有0.3元左右的收益。这一篇开始介绍如何使用Midjourney制作小红书壁纸。


一、制作步骤


使用Midjourney制作小红书壁纸的步骤比较简单,分为5步,其中最关键的步骤就是画出用户喜欢的壁纸


1、绘画壁纸


在开始绘画之前,我们首先要确定好壁纸类型,小红书的壁纸类型有多种,包括剪纸类型、花草类型、梵高类型(使用梵高的风格画各种夜景)、二次元类型等,确定壁纸类型有利于我们吸引垂类粉丝。


以二次元风格壁纸为例,在Midjourney中,输入/image指令,再输入提示词:cute cat swimming underwater,smiling,bright eyes,portrait,dream,a bright color --ar 9:16 --q 2 --niji 5 --v 5.1 --s 750,就可以得到一张好看的壁纸。 



2、添加边框


添加手机边框有两种方式:一种需要熟悉PS软件,使用PSD文件添加;另一种是通过醒图、美图秀秀等APP添加(搜索【手机边框】,找到合适的边框,进行添加)。这一步骤不是必须的,但添加之后,会有仪式感。 



3、发布作品


接下来就可以在小红书发布作品,标题填写【4K高清壁纸】,有利于用户搜索。如果会写文案,也可以进行文案的添加。需要说明的是,一开始不建议开通店铺,等用户数到达一定数量再开通(不会开通小红书店铺的读者,可以留言)。


4、上传图片


如果通过小程序进行变现,需要将生成的壁纸上传到小程序,以XX壁纸小程序为例,抖音搜索或者微信搜索都可以,之后注册,注册完成设置自己的口令,口令尽量以数字为主,这样用户下载起来比较方便。


5、进行引流


如果开通了商城,发布作品时,将商城添加到作品中即可;如果是小程序,需要进行引流,比如,进入粉丝群可以得到高清图片、三连击得到原图壁纸等。


二、发布逻辑


上述5个步骤实现了小红书壁纸的制作以及发布,如何源源不断的画出用户喜欢的壁纸,也是我们需要考虑的问题。


1、模仿


模仿点赞比较高的作品,火过的作品有可能再火一次


2、创新


创新需要具备一定的审美能力,不过没有关系,多画画就有了。


3、追随实事


这和那些口播博主一样,追随社会热点事件,只不过我们追随的是节日氛围,比如在端午节发布和其相关的作品。


4、多发


做过自媒体的都知道,作品是否能火,多少有一点玄学,也就是概率事件。那对抗概率事件最好的武器就是数量和时间,也就是多发布作品,坚持一两个月。


以上四点是做自媒体的通用逻辑,就不再赘述。


三、案例


在小红书中,通过Midjourney制作壁纸变现超过5000元的大有人在,以下便是几个案例:


1、两个月,变现15000元。




2、两个月,变现28000元。




3、一个多月,变现8000元(包含80件商品)。




列举这几个案例,并不是说我们按照上述步骤就一定能达到这样的收入(赚钱本身就不是容易的事),而是想要表达两个意思:第一,在找对标账号的时候,我们就应该找这种变现能力强的账号,跟着他们学习,收获会很大。


第二,任何技术越早开始越好,AI已经是势不可挡,因为它的本质是生产力的提高,而且国家也在鼓励人工智能的发展,作为技术人员,我们有先发优势,当机会来临的时候,我们一定要抓住。


就像在掘金社区,有人是为了分享技术,但有人也是为了吸引粉丝,进行副业变现。当某个行业红利不在的时候,最好的方法是寻找下一个行业红利。


我是阿凯,一位不知名的Midjourney玩家,如果你有任何关于Midjourney的问题,可以进行留言。关注我,下一篇手把手教你使用Midjourney制作抖音壁纸,进行副业变现。


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

浅谈一下滴滴实习

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。 说实话我这文章写...
继续阅读 »

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。
说实话我这文章写得毫无章法,完全是想到哪里写到哪里,也不想去分门别类了,我觉得真实的想法最重要,如果有语义错误就略过吧哈哈。


说说业务


对这段实习做一个小小的总结,先说一说业务吧,我所在的是滴滴商旅的一个技术部门,主要负责企业版这块的业务,我去的第一天上午看团队规范文档,下午跑项目看代码,第二天接需求,当然是比较简单的需求,后面陆陆续续做了滴滴企业版的小部分 pc 端官网和大部分移动端官网,如果你现在用手机搜滴滴企业版,那么你看到的页面大概率就是我做的,除此以外还有一个经典后台管理项目,其实项目用的技术栈都还好,没有说很有难度的,对于业务来说我觉得最难的应该就是项目的搭建和部署,然后就是技术方案,开发代码确实是最基础的事情了,这几个月完成的代码量并不大,这也完全在意料之中,实习生嘛,能确保自学到东西就行,当自学到一定的程度会很迷茫,不知道下一个进阶的领域是什么,但是在这段时间我逐渐感觉前端的一个瓶颈就在前端工程化,其实早就在学了,但是没有实际的项目经验加上网上教程比较匮乏,大多是讲解 webpack 的基本使用甚至一度让大部分人认为 webpack 就是前端工程化,如果有后端基础我觉得理解工程化那就太简单了,只不过可惜的是参与前端开发的大多是后端经验为 0 的同学,因此对于常年在浏览器玩 js 的我们很难理解在编译阶段能做的一些工作的意义所在,不管是现在的 Node 或是 Go 和 Rust,其实都可以作为一个深入挖掘的方向,至少我感觉业务是真的很无聊,偶尔当玩具写写还行,每天写真的没意思。


除了业务以外认知到一些原来不知道的职场"内幕"。


第一点:面试冷知识

走之前组内一直在招社招的员工,当面试官的兄弟和我说了我才知道,原来面试通一个部门甚至是同一个面试官可能真的会因为面试官心情或者其余外在因素决定你面试是否通过,比如最近部门比较忙,那可能面试也就比较水一点,大概考察没问题就直接过了没有那么多的时间去做横向比较(那我面的部门基本都还是比较闲啊哈哈),又或者是面试官看你比较顺眼性格也比较符合他的要求,大概率会给一些比较简单的题,这些都会影响面试官的判断从而决定你是否能通过面试最终拿 offer,所以经过这件事之后看开了很多,如果原来你一直不理解平时技术没你好的同学最后能拿到同公司或者同部门 offer,现在应该慢慢也就看开了,一旦挂了及时投递下一个部门,这不一定是自己的原因。


第二点:大厂其实不全是 996

不要被危言耸听,这其实大概率取决于你的部门而非公司,我在的部门经过这两年的形势成功变得小而精,小组的氛围很好,平时开发大家都合作得很开心,不管是导师还是 leader,休息了也会偶尔一起打打游戏,在这个部门我感觉挺好至少没有看到所谓的大厂 996,基本上大家 10 点来,最晚 8 点也都走了。离职的前一天刚好赶上了部门团建于是狠狠地去蹭了一只烤全羊,leader 把商旅的大 leader 请过来了,我对大领导的刻板印象是电视里那种懂得都得,但是没想到和我想的完全相反,大家举杯畅饮吹牛逼,欢声笑语,挺好,,后来想想有可能是因为大家都是技术出身很多时候也都很讨厌那一套,这也是我对互联网最满意的一点,凭本事吃饭,对于出身不是那么地依赖,也不是尔诈我虞,阿谀奉承。


第三点:学会装菜,不要没事找事

作为实习生,懂的都懂,其实在哪里都一样,如果你太着急表现自己,别人就会觉得你过分刺毛,能装菜的地方千万别装逼,艹,我感觉我就是傻逼了,这也许也是我离开的原因之一,作为实习生老老实实完成自己的工作就好,能够保质保量完成任务对于导师来说基本就差不多了,至于一些 pua 话术里面说的额外价值,我觉得对于没有转正的实习生来说毫无意义,反而会自找麻烦,因为并不会因为你原本安排 2 天切完的图你一天切完导师就给你放松自学,很多时候你做的事情是否有意义完全取决于你的导师是否愿意安排有意义的工作给你,所幸我在滴滴完整地参与了项目的技术方案到代码编写直至最后部署上线,里面沉淀了我自己的思考,经过这段实习确实让我受益匪浅。


最后一点是软实力

我觉得这也是我在这段实习中收获到的最重要的东西之一:"学会总结,及时复盘",每次周会给导师和兄弟们讲方案总是要准备很久,会去看很多的自己不知道的东西,以此来让我写的东西显得足够的高大上,记得有一次上线官网出问题了,意料之中做了一个复盘,倒不是说学到了什么代码层面的东西,更多的是让我了解了整个项目从开发到部署上线的流程,这个远远比写代码有意义,不得不说这极大地培养了我的能力,包括新技术的敏感程度,技术的深度以及口才,总结出来的东西一方面加深了自己的记忆和理解,往小了说,让我可以在以后的技术面试中就这段经历侃侃而谈,往大了说,这个让我学会从更高的视角去看问题,不再是盯着代码的一亩三分地,更多的是学会从项目的技术架构层面去看问题,第二是学会表现自己并且及时纠正自己的错误,没错,就是给别人看,自己瞎学总结是没有意义的,你是一个无比努力的人,可是大家不知道那也毫无意义,他能知道的仅仅是你能写上简历的东西,只有向别人更好的展示自己,下一次面试官看到你才会觉得你是一个善于总结和反思的人,程序员这一行也是这样,其实参与一个开源项目远远比你基础扎实更让人刮目相看,尽管你只是为一个看起来无比高大上的开源切了图,对我自己来说我只是把曾经在 wps 或者 typora 的写作 转移到了掘金或者 github,内容并没有太大的变化,这样的事情何乐而不为呢。


最后做一个收尾。


这两天想去北京附近转转,今天跑到了天安门,还是想吐槽地铁站一些人地素质问题,经典的钱包鼓起来了素质教育没跟上来,或者换句话说富起来很多并非接受过良好教育的一批人。


马上快开学了,要回学校拿保研名额,说实话我到现在都不确定哪条路是对了,大厂?还是保研?还是国企公务员?谁知道呢,每个人有每个人的说法,老员工会劝你保研进编制,新员工会劝你尽早进大厂捞钱,每个人追求的往往都是目前最缺失的,也许正是因为未来充满未知所以才无限期待,不然像我这样躺尸一周那该多无聊,脑袋都睡麻木了,这两周陆陆续续也面试了四五家公司,不得不说有大厂背书投简历就是好使,曾经拒绝过我的那些公司都拿到了 offer 然后全给拒了哈哈,不为别的就是解气,基本都是一些 b 格还比较高的独角兽公司,比如教育,云服务器,游戏行业等等,大厂我肯定还是没这个底气的哈哈我依然是大部分大厂的舔狗,不过结果不算坏,下一站是老铁厂了。


不知道是否会有之前一起工作的兄弟看到这篇文章,如果认为我有说得不恰当的地方欢迎指正。


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

不知什么原因,背调没过?

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。 在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离...
继续阅读 »

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。


在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离职,新公司还没入职,背调没通过,那就有点悲催了。所以,今天就跟大家聊聊入职背调中的一些注意事项。


第一,背调日趋严格


整体而言,背调是越来越严格了。当然,每家公司都不是为了背调而背调,这是劳民伤财的事,主要是因为履历包装的情况太严重。特别是有一部分刚毕业为了找到工作,通过简历、履历、学历等途径包装成2-3工作经验的情况时有发生。


还有就是,HR也有考核指标,HR在实际招聘的过程中会踩一些坑,为了避免类似的事情发生,会在既有的经验上进行迭代筛查条件。


一般背调有两种方式:体量小一些的公司,HR会给你留电话的人打电话核实;体量大一些的公司会直接委托三方来进行背调核实。


HR直接打电话的背调相对来说会简单一些,而且会有一些个人风格,我们暂且不提。而背调公司的风格一般比较统一。


第二,背调联系人


背调的过程一般会让写三类联系人:直接领导、人力和同事。大概率会背调之前两家公司的履历。


在填写时,你就需要慎重考虑了,基本上会挨个打电话询问你的情况的。所以,你写谁之前,最好先打个招呼,否则说你的坏话,那你就有些悲催了。像上篇文章中同事那样不打招呼的操作,是强烈不建议的。


另外,你写的这些联系人要能够联系得到才行。如果都联系不上,过的可能性就不大了。


第三,背调的过程


曾经多次作为上级领导参与背调,背调的核心点有几项(他,代表被背调的人):


确认身份:确认你是否是本人,是否是他的上级领导。同时,还会确认他的岗位信息,他是否带下属,下属多少人等。除了电话确认之外,甚至还会要求入职人员跟相关人要公司企业管理软件(钉钉、飞书等)中带有企业名称、填写人姓名的截图证明等。


表现评分:在工作表现、沟通表现等方面会有1-10分,询问各项的表现评分是多少。同时,在问题的涉及上还会有一些交叉认证的小策略。会涉及到:工作表现如何,与大家相处的如何,吃苦耐劳能力如何,抗压能力如何、离职原因是什么、你是否满意他的整体表现、是否有违规操作等等。


交叉确认:除了个人表现的评分确认之外,如果同一个公司的背调,还会交叉确认一下你留的其他人员是否也是这家公司的,是否是对应岗位的。


如果你预留的信息都是真实的,那么不用担心什么,跟填写联系人的打好招呼就行了。如果部分内容有出入,那可要交代清楚了。


另外,在工作中,平时与同事和上下级相处时,保持融洽的关系,留一个联系方式等也有一定的必要性。


第四,其他可能性


除了上面统一的背调流程之外,某些公司还会有更加严格的背调信息。这些信息是否违法违规暂且不说,但是是会出现的。如果你不care这份工作,可以拒绝提供的。


常见的有收入证明、工资流水、社保缴纳、征信报告等。


收入证明一般由上家公司出具并盖章,私企或关系比较好一些,可以适当调整。工资流水可以是银行打印的或下载的电子单据。社保缴纳可以提供查询到的流水。征信报告这个对于部分金融相关的行业会有一定要求,会引导你操作申请一份个人征信报告。


另外还有两项,大多数人可能不知道,但对于高端的一些岗位也会涉及到:HR的圈子和劳动诉讼。


HR是有自己的圈子和人脉的,而且可能比你想象的要广。如果你在上家公司,或者在圈子里名声不好,很可能会被问出来的。这个也没其他办法,自己的个人人设和职业素养问题了。


另外一个就是劳动诉讼,这个也是可以调查出来的,除了有专门的机构可以做这些事之外,某些诉讼可以在企业的“法律诉讼”中查到诉讼的另一方的。当然,如果曾经涉及到刑事案件用人单位也是可以查出来的。


最后


市场越来越卷,而打工人越来越不容易。在日常工作中保持良好的人际关系和职业素养,更多的还是为自己铺好后路。在面试找新工作时保持诚信,尽量避免出现撒一个谎,用一百个谎来圆的情况。


最后,无论怎样,都要有备选方案,既不能丢了西瓜捡了芝麻,更不能最后两手空空。


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

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思


  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。
  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!
  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!

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

iOS 内存泄漏排查方法及原因分析

iOS
本文将从以下两个层面解决iOS内存泄漏问题:内存泄漏排查方法(工具)内存泄漏原因分析(解决方案) 在正式开始前,我们先区分两个基本概念: 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直...
继续阅读 »

本文将从以下两个层面解决iOS内存泄漏问题:

  • 内存泄漏排查方法(工具)
  • 内存泄漏原因分析(解决方案)


在正式开始前,我们先区分两个基本概念:


  • 内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。 一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash。(因此,开发中我们要尽量避免内存泄漏的出现)
  • 内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。 通俗理解就是内存不够用了,通常在运行大型应用或游戏时,应用或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。最终导致机器重启或者程序crash


简单来说:





一、排查方法



我们知道,iOS开发有“ARC机制”帮忙管理内存,但在实际开发中,如果处理不好堆空间上的内存还是会存在内存泄漏的问题。如果内存泄漏严重,最终会导致程序的崩溃。



首先,我们需要检查我们的App有没有内存泄漏,并且快速定位到内存泄漏的代码。目前比较常用的内存泄漏的排查方法有两种,都在Xcode中可以直接使用:


  • 第一种:静态分析方法(Analyze
  • 第二种:动态分析方法(Instrument工具库里的Leaks)。一般推荐使用第二种。

1.1 静态内存泄漏分析方法:


  • 第一步:通过Xcode打开项目,然后点击Product->Analyze,开始进入静态内存泄漏分析。 如下图所示:

  • 第二步:等待分析结果。

  • 第三步:根据分析的结果对可能造成内存泄漏的代码进行排查,如下图所示。



PS:静态内存泄漏分析能发现大部分问题,但只是静态分析,并且并不准确,只是有可能发生内存泄漏。一些动态内存分配的情形并没有分析。如果需要更精准一些,那就要用到下面要介绍的动态内存泄漏分析方法(Instruments工具中的Leaks方法)进行排查。



1.2 动态内存泄漏分析方法:



静态内存泄漏分析不能把所有的内存泄漏排查出来,因为有的内存泄漏发生在运行时,当用户做某些操作时才发生内存泄漏。这是就要使用动态内存泄漏检测方法了。



步骤如下:


  • 第一步:通过Xcode打开项目,然后点击Product->Profile,如下图所示:

    • 第二步:按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮。如下图:

    • 第三步:这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:


点击左上角的红色圆点,这时项目开始启动了,由于Leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。



选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。


举个例子:



PS:AFHTTPSessionManager内存泄漏是一个很常见的问题:解决方法有两种:点击这里





二、内存泄漏的原因分析


目前,在ARC环境下,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,最终导致dealloc()方法无法被调用。主要原因大概有一下几种类型:


2.1 ViewController中存在NSTimer


如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用

[NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

  • 理由:这时 target: self,增加了ViewController的retain count, 即self强引用timertimer强引用self。造成循环引用。
  • 解决方案:在恰当时机调用[timer invalidate]即可。

2.2 ViewController中的代理delegate



代理在一般情况下,需要使用weak修饰。如果你这个VC需要外部传某个delegate进来,通过delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量使用weak修饰,否则你的VC会持续持有这个delegate,直到代理自身被释放。



  • 理由:如果代理用strong修饰,ViewController(self)会强引用ViewView强引用delegatedelegate内部强引用ViewController(self)。造成内存泄漏。
  • 解决方案:代理尽量使用weak修饰。

举个例子:代理一般用weak修饰,避免循环引用。

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;

- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画

2.3 ViewController中Block



在我们日常开发中,如果block使用不当,很容易导致内存泄漏。



  • 理由:如果block被当前ViewController(self)持有,这时,如果block内部再持有ViewController(self),就会造成循环引用。
  • 解决方案:在block外部对弱化self,再在block内部强化已经弱化的weakSelf

For Example:

    __weak typeof(self) weakSelf = self;

[self.operationQueue addOperationWithBlock:^{

__strong typeof(weakSelf) strongSelf = weakSelf;

if (completionHandler) {

KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);

completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];

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

如何判断设备是否越狱?

iOS
前言 iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。 但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一...
继续阅读 »

前言


iPhone 越狱已经不是什么新鲜事,但是越狱之后意味着已经拿到了系统的所有权限,继续在越狱的设备上运行你的程序也就意味着不再安全,因此目前很多主流的 App 都是禁止运行在此类设备上的。


但是怎么判断一个设备是否为越狱的机器呢?今天就来讲讲我所知道的一些方法。


方法一


检查手机上是否安装了 Cydia,玩越狱的同学肯定都清楚,这个 app 堪称是越狱系统的 App Store,上边可以安装各种正规 App Store 安装不到的软件。Cydia 上除了独立的应用程序之外,更多的包是 iOS 本身和应用程序的扩展、修改和主题。


因此可以说只要是越狱的设备,都会安装这个应用,那我们只需要检测这个应用存不存在就行了。


这里主要用到的方法是用 canOpenURL 是否能打开 cydia:// 这个 URL Scheme。

func isJailBreak() -> Bool {
    return UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
}



这里记得把 cydia 加入到 info.plist 中的 LSApplicationQueriesSchemes 字段里才能正常检测应用是否安装



这种方式简单粗暴,不过不建议用,因为准确度可能不高,一方面 cydia 可能把这个 URL Scheme 改掉防止你检测。另一方面正常手机也可能会有一个 app 的 URL Scheme 叫这个名字,造成误判。


方法二


检测是否存在 MobileSubstrate 动态库,这个库是 cydia 的基石,越狱环境下安装绝大部分插件,必须要有 MobileSubstrate,因此我们只需要判断是否存在这个动态库即可。


我在网上找了一个 c 语言的实现:

bool
isJailBreak(void)
{
    const char *const imageName = "MobileSubstrate";
    if (imageName != NULL) {
        const uint32_t imageCount = _dyld_image_count();
        for (uint32_t iImg = 0; iImg < imageCount; iImg++) {
            const char *name = _dyld_get_image_name(iImg);
            if (strstr(name, imageName) != NULL) {
                return true;
            }
        }
    }
    return false;
}


方法三


还是检测文件,如果越狱的话,设备会创建许多文件,可以使用 FileManager 来检测这些文件是否存在:

func isJailBreak() -> Bool {
#if targetEnvironment(simulator)
    return false
#else
    let files = [
        "/private/var/lib/apt",
        "/Applications/Cydia.app",
        "/Applications/RockApp.app",
        "/Applications/Icy.app",
        "/Applications/WinterBoard.app",
        "/Applications/SBSetttings.app",
        "/Applications/blackra1n.app",
        "/Applications/IntelliScreen.app",
        "/Applications/Snoop-itConfig.app",
        "/bin/sh",
        "/usr/libexec/sftp-server",
        "/usr/libexec/ssh-keysign /Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt /System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
        "/System/Library/LaunchDaemons/com.ikey.bbot.plist",
        "/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
        "/Library/MobileSubstrate/DynamicLibraries/Veency.plist"
    ]
    return files.contains(where: {
        return FileManager.default.fileExists(atPath: $0)
    })
#endif
}


这里有个条件编译,在模拟器下是不需要检查的。


方法四


越狱之后所有 App 都被授予 root 权限,并且可以修改沙箱之外的文件。利用这个特点,如果我们的 App 可以写入其沙箱之外的文件,则证明该设备已越狱:

func isJailBreak() -> Bool {
    let string = "iOS 新知"
    do {
        try string.write(to: URL(filePath: "/private/myfile.txt"), atomically: true, encoding: .utf8)
        return true
    } catch {
        return false
    }
}


方法五


越狱之后也就意味着 App 可以随意调用系统 API 了,因此我们可以尝试调用系统 API,来查看是否能得到正确结果,以此来判断是否越狱:

func isJailBreak() -> Bool {
    let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
    let forkPtr = dlsym(RTLD_DEFAULT, "fork")
    typealias ForkType = @convention(c) () -> Int32
    let fork = unsafeBitCast(forkPtr, to: ForkType.self)

    return fork() != -1
}


建议以上五种方法结合使用,提高检测的准确率


检测到越狱设备,禁止使用


如果检测到当前运行环境为越狱设备,可以强制退出 App,以确保安全。强制退出 app 的方法就很多了,可以使用 exit(-1),也可以人为做个数组越界之类的:

// 检测到越狱设备
if isJailBreak() {
    // 退出 app
    exit(-1)
    // 或者数组越界 crash
    // [0][1]
}

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

iOS气泡提示工具BubblePopup的使用

iOS
BubblePopup 气泡弹框,气泡提示框,可用于新手引导,功能提示。 在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。 使用...
继续阅读 »

BubblePopup


气泡弹框,气泡提示框,可用于新手引导,功能提示。


在平时的开发中,通常新手引导页或功能提示页会出现气泡弹窗来做提示。如果遇到了这类功能通常需要花费一定的精力来写这么一个工具的,这里写了一个气泡弹窗工具,希望能帮你提升一些开发效率。


使用方法

  • 从gitHub上下载代码到本地,代码地址:github.com/zhfei/Bubbl…
  • 调用BubblePopupManager文件内的单例方法,在指定的页面上添加气泡提示。 普通文本气泡弹窗使用方式如下:
BubblePopupManager.shared.addPopup(toView: self.view, tips: "冒泡弹窗", popupType: .dotLine, positionType: .bottom, popupPoint: nil, linkPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), maxWidth: 200.0)


自定义View气泡弹窗使用方式如下:

BubblePopupManager.shared.addPopup(toView: self.view, customContentView: MyContentView(), popupType: .triangle, positionType: .bottom, popupPoint: CGPoint(x: sender.frame.midX, y: sender.frame.minY), linkPoint: nil, maxWidth: 200.0)

注意:自定义内容View只能使用frame布局,不能使用约束。


设计模式


气泡弹窗View的结构设计采用的设计模式为组合模式

把气泡弹窗分为3个部分:气泡背景,气泡指示器,气泡提示内容。


在创建气泡弹窗时,根据子类的自定义实现,将这三部分分别创建并组装到一起。实现了功能的灵活插拔和自定义扩展。


气泡弹窗View类图



气泡弹窗生成算法采用的设计模式为模版方法模式

在气泡构建基类中设置好气泡的构建步骤,把必要的部分或者提供默认实现的部分在父类中提供默认的实现,对其他需要自定义实现的部分,只在父类中写了一个抽象方法,具体实现交给子类自己实现。


虚线气泡弹窗类图



三角形气泡弹窗类图



核心实现

  • BubblePopupManager: 使用气泡弹窗工具的入口,通过它创建并添加一个气泡弹窗到指定的View上。

  • BubblePopupBuilder: 气泡弹窗构建者基类,使用模版方法模式定义了气泡的构建流程,子类可以自定义各自的实现。

  • DotLineBubblePopupBuilder: 虚线气泡弹窗基类,它是基类BubblePopupBuilder的子类,内部包含了虚线气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right虚线气泡弹窗。

  • TriangleBubblePopupBuilder : 三角形气泡弹窗基类,它是BubblePopupBuilder的子类,内部包含了三角形气泡弹窗生成时所需要的工具方法和必要属性,方便创建top,bottom,left,right三角形气泡弹窗

  • BubblePopup: 气泡弹窗View,它内部使用组合模式将子部件组合起来,组成了一个气泡弹窗。

  • BubbleViewFactory: 气泡弹窗子视图创建工程,用于创建气泡弹窗所需要的子视图,并将各个子视图组装成一个最终的气泡弹窗。


BubblePopupBuilder

BubblePopupBuilder是所有气泡弹窗的公共基类,对于里面定义的属性和方法的功能分别为


  • 属性:属性里保存的是气泡弹窗公共的,必要的数据。
  • 方法:在基类提供的方法中主要用于定义气泡的构建流程。 核心方法如下:
   func setupUI() {
addBubbleContentView(to: bubblePopup)
addBubbleBGView(to: bubblePopup)
updateLayout(to: bubblePopup)
addBubbleFlagView(to: bubblePopup)
}

其中气泡内容展示视图和气泡背景视图有默认实现,子类可以直接使用默认样式。


而气泡标识View和气泡布局方法则需要子类自己实现,因为不同类型的气泡弹窗它们的气泡标识设布局方式是不一样的。


DotLineBubblePopupBuilder

虚线气泡基类DotLineBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:增加了虚线弹窗必要的linkPoint属性,即:虚线与气泡弹窗的连接点。 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawDotLineLayerRectParams

用于虚线图层绘制:获取虚线绘制时所需要的绘制元素坐标,如:虚线的开始,结束坐标,连接点圆的直径等。

getDotLineLayerContainerViewFrame

更新虚线容器View的位置大小信息:获取不同情况下的虚线容器Frame。

layoutDotLineBubblePopupView

更新虚线气泡弹窗的frame。

updateBGBubbleViewFrame

更新气泡背景的frame。


这里提供的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果。这里按道理可以使用设计模式中策略模式来对算法进行封装,如:在基类定义一个抽象方法,将上面则4个工具方法分拆到各自的子类中,让子类在对应的自己的类中实现这个方法。


这里没有这样做原因是:这些方法在子类中的实现代码并不复杂,用一个方法根据条件集中返回是比较方便的,而分拆到不同类中反而很麻烦。所以选择在基类中以方法工具的形式统一放置了。


DotLineTopBubblePopupBuilder

top型虚线气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面两个方法进行了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class DotLineTopBubblePopupBuilder: DotLineBubblePopupBuilder {

override func updateLayout(to bubblePopup: BubblePopup) {
layoutDotLineBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}

override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getDotLineLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawDotLineLayerRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateDotLineBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}

}

其他bottom, left, right类型相似。


TriangleBubblePopupBuilder

三角形气泡基类TriangleBubblePopupBuilder,它继承自BubblePopupBuilder

  • 属性:相对于基类增加了popupPoint属性,它是三角形顶点指向的坐标点 增加了一个坐标系转换懒加载属性,用于将用户设置的屏幕坐标点转成气泡内部的视图坐标系中的点。

  • 重要方法说明:

getDrawTriangleLayeyRectParams

为三角形图层绘制提供不同气泡类型所需要的绘制元素坐标,如:三角形的三个顶点。

getTriangleLayerContainerViewFrame

获取不同情况下三角形图层容器的Frame,用于更新三角形图层容器View的位置大小。

layoutTriangleBubblePopupView

更新三角形气泡弹窗的frame。

updateTriangleBGBubbleView

更新气泡背景的frame。


三角形弹窗基类TriangleBubblePopupBuilder的设计方式和虚线弹窗基类是一样的。
这里的方法属于工具方法,子类可以通过传递自己的类型来得到对应的结果,通过牺牲一点开发模式的规范化来换取开发效率的提升。


在三角形气泡基类的下面同样有4个子类top,bottom,left ,right进行各种的自定义实现。


TriangleTopBubblePopupBuilder

top型三角形气泡弹窗DotLineTopBubblePopupBuilder,它继承自DotLineBubblePopupBuilder,属于一直具体的弹窗类型。


它里面只对下面这两个方法做了重写,根据自己的类型进行子类个性化实现。

override func updateLayout
override func addBubbleFlagView

具体实现如下:

class TriangleTopBubblePopupBuilder: TriangleBubblePopupBuilder {
override func updateLayout(to bubblePopup: BubblePopup) {
layoutTriangleBubblePopupView(bubblePopup: bubblePopup, positionType: .top)
}
override func addBubbleFlagView(to bubblePopup: BubblePopup) {
assert(!self.targetPoint.equalTo(.zero), "气泡提示点无效")

let flagFrame = getTriangleLayerContainerViewFrame(position: .top, targetPoint: self.targetPoint)
let params = getDrawTriangleLayeyRectParams(position: .top)
let flagBubbleView = BubbleViewFactory.generateTriangleBubbleFlagView(flagFrame: flagFrame, position: .top, params: params)
bubblePopup.bubbleFlagView = flagBubbleView
bubblePopup.addSubview(flagBubbleView)
}
}

其他bottom, left, right类型相似。


弹窗效果展示


三角形气泡弹窗



虚线气泡弹窗



自定义气泡弹窗



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

用一个RecyclerView实现抖音二级评论

前一阵,看到一位掘友分享了一篇文章:Android简单的两级评论功能实现,看得出来,是一位Android萌新记录的学习过程。我当时还留了一条建议: 建议用单RecyclerView+多ItemType+ListAdapter实现,保持UI层的清洁,把逻辑处理...
继续阅读 »

前一阵,看到一位掘友分享了一篇文章:Android简单的两级评论功能实现,看得出来,是一位Android萌新记录的学习过程。我当时还留了一条建议:



建议用单RecyclerView+多ItemType+ListAdapter实现,保持UI层的清洁,把逻辑处理集中在数据源的转换上,比如展开/收起二级评论可以利用flatMap和groupBy等操作符转换



其实我之前在工作中,也曾经做过类似抖音的二级评论的需求。但那个时候自己很菜,还没有用过Kotlin,协程更是没有接触过,这个功能和另一位同事一起开发了两周才搞定。


刚好这个周末没啥事,就想着写一个简单实现抖音二级评论基本功能的Demo。一方面,想试试自己现在开发这样一个需求会是什么样的体验;另一方面,也算是给Android掘友,尤其是萌新,分享一点业务开发的心得。


先上个效果图(没有UI,将就看吧),写代码的整个过程花了4个小时左右,相比当初自己开发需求已经快了很多了哈。



给产品估个两天时间,摸一天半的鱼不过分吧(手动斜眼)



评论.gif


需求拆分


这种大家常用的评论功能其实也就没啥好拆分的了,简单列一下:



  • 默认展示一级评论和二级评论中的热评,可以上拉加载更多。

  • 二级评论超过两条时,可以点击展开加载更多二级评论,展开后可以点击收起折叠到初始状态。

  • 回复评论后插入到该评论的下方。


技术选型


前面我在给掘友的评论中,也提到了技术选型的要点:


单RecyclerView + 多ItemType + ListAdapter


这是基本的UI框架。


为啥要只用一个RecyclerView?最重要的原因,就是在RecyclerView中嵌套同方向RecyclerView,会有性能问题和滑动冲突。其次,当下声明式UI正是各方大佬推崇的最佳开发实践之一,虽然我们没有使用声明式UI基础上开发的Compose/Flutter技术,但其构建思想仍然对我们的开发具有一定的指导意义。我猜测,androidx.recyclerview.widget.ListAdapter可能也是响应声明式UI号召的一个针对RecyclerView的解决方案吧。


数据源的转换


数据驱动UI!


既然选用了ListAdapter,那么我们就不应该再手动操作adapter的数据,再用各种notifyXxx方法来更新列表了。更提倡的做法是,基于data class浅拷贝,用Collection操作符对数据源的进行转换,然后将转换后的数据提交到adapter。为了提高数据转换性能,我们可以基于协程进行异步处理。


graph LR
start[原List] --异步数据处理--> 新List --> stop[ListAdapter.submitList]
stop --> start

要点:



  • 浅拷贝


低成本生成一个全新的对象,以保证数据源的安全性。


data class Foo(val id: Int, val content: String)

val foo1 = Foo(0, "content")
val foo2 = foo1.copy(content = "updated content")


  • Collection操作符


Kotlin中提供了大量非常好用的Collection操作符,能灵活使用的话,非常有利于咱们向声明式UI转型。


前面我提到了groupByflatMap这两个操作符。怎么使用呢?


以这个需求为例,我们需要显示一级评论、二级评论和展开更多按钮,想要分别用一个data class来表示,但是后端返回的数据中又没有“展开更多”这样的数据,就可以这样处理:


// 从后端获取的数据List,包括有一级评论和二级评论,二级评论的parentId就等于一级评论的id
val loaded: List<CommentItem> = ...
val grouped = loaded.groupBy {
// (1) 以一级评论的id为key,把源list分组为一个Map<Int, List<CommentItem>>
(it as? CommentItem.Level1)?.id ?: (it as? CommentItem.Level2)?.parentId
?: throw IllegalArgumentException("invalid comment item")
}.flatMap {
// (2) 展开前面的map,展开时就可以在每级一级评论的二级评论后面添加一个控制“展开更多”的Item
it.value + CommentItem.Folding(
parentId = it.key,
)
}


  • 异步处理


前面我们描述的数据源的转换过程,在Kotlin中,可以简单地被抽象为一个操作:


List<CommentItem>.() -> List<CommentItem>

对于这个需求,数据源转换操作就包括了:分页加载,展开二级评论,收起二级评论,回复评论等。按照惯例,抽象一个接口出来。既然我们要在协程框架下进行异步处理,需要给这个操作加一个suspend关键字。


interface Reducer {
val reduce: suspend List<CommentItem>.() -> List<CommentItem>
}

为啥我给这个接口取名Reducer?如果你知道它的意思,说明你可能已经了解过MVI架构了;如果你还不知道它的意思,说明你可以去了解一下MVI了。哈哈!


不过今天不谈MVI,对于这样一个小Demo,完全没必要上架构。但是,优秀架构为我们提供的代码构建思路是有必要的!


这个Reducer,在这里就算是咱们的小小业务架构了。



  • 异步2.0


前面谈到异步,我们印象中可能主要是网络请求、数据库/文件读写等IO操作。


这里我想要延伸一下。


ActivitystartActivityForResult/onActivityResultDialog的拉起/回调,其实也可以看着是异步操作。异步与是否在主线程无关,而在于是否是实时返回结果。毕竟在主线程上跳转到其他页面,获取数据再回调回去使用,也是花了时间的啊。所以在协程的框架下,有一个更适合描述异步的词语:挂起(suspend)


说这有啥用呢?仍以这个需求为例,我们点击“回复”后拉起一个对话框,输入评论确认后回调给Activity,再进行网络请求:


class ReplyDialog(context: Context, private val callback: (String) -> Unit) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_reply)
val editText = findViewById<EditText>(R.id.content)
findViewById<Button>(R.id.submit).setOnClickListener {
if (editText.text.toString().isBlank()) {
Toast.makeText(context, "评论不能为空", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
callback.invoke(editText.text.toString())
dismiss()
}
}
}

suspend List<CommentItem>.() -> List<CommentItem> = {
val content = withContext(Dispatchers.Main) {
// 由于整个转换过程是在IO线程进行,Dialog相关操作需要转换到主线程操作
suspendCoroutine { continuation ->
ReplyDialog(context) {
continuation.resume(it)
}.show()
}
}
...进行其他操作,如网络请求
}

技术选型,或者说技术框架,咱们就实现了,甚至还谈到了部分细节了。接下来进行完整实现细节分享。


实现细节


MainActivity


基于上一章节的技术选型,咱们的MainActivity的完整代码就是这样了。


class MainActivity : AppCompatActivity() {
private lateinit var commentAdapter: CommentAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
commentAdapter = CommentAdapter {
lifecycleScope.launchWhenResumed {
val newList = withContext(Dispatchers.IO) {
reduce.invoke(commentAdapter.currentList)
}
val firstSubmit = commentAdapter.itemCount == 1
commentAdapter.submitList(newList) {
// 这里是为了处理submitList后,列表滑动位置不对的问题
if (firstSubmit) {
recyclerView.scrollToPosition(0)
} else if (this@CommentAdapter is FoldReducer) {
val index = commentAdapter.currentList.indexOf(this@CommentAdapter.folding)
recyclerView.scrollToPosition(index)
}
}
}
}
recyclerView.adapter = commentAdapter
}
}

RecyclerView设置一个CommentAdapter就行了,回调时也只需要把回调过来的Reducer调度到IO线程跑一下,得到新的数据listsubmitList就完事了。如果不是submitList后有列表的定位问题,代码还能更精简。如果有知道更好的解决办法的朋友,麻烦留言分享一下,感谢!


CommentAdapter


别以为我把逻辑处理扔到adapter中了哦!


AdapterViewHolder都是UI组件,我们也需要尽量保持它们的清洁。


贴一下CommentAdapter


class CommentAdapter(private val reduceBlock: Reducer.() -> Unit) :
ListAdapter<CommentItem, VH>(object : DiffUtil.ItemCallback<CommentItem>() {
override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {
if (oldItem::class.java != newItem::class.java) return false
return (oldItem as? CommentItem.Level1) == (newItem as? CommentItem.Level1)
|| (oldItem as? CommentItem.Level2) == (newItem as? CommentItem.Level2)
|| (oldItem as? CommentItem.Folding) == (newItem as? CommentItem.Folding)
|| (oldItem as? CommentItem.Loading) == (newItem as? CommentItem.Loading)
}
}) {

init {
submitList(listOf(CommentItem.Loading(page = 0, CommentItem.Loading.State.IDLE)))
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_LEVEL1 -> Level1VH(
inflater.inflate(R.layout.item_comment_level_1, parent, false),
reduceBlock
)

TYPE_LEVEL2 -> Level2VH(
inflater.inflate(R.layout.item_comment_level_2, parent, false),
reduceBlock
)

TYPE_LOADING -> LoadingVH(
inflater.inflate(
R.layout.item_comment_loading,
parent,
false
), reduceBlock
)

else -> FoldingVH(
inflater.inflate(R.layout.item_comment_folding, parent, false),
reduceBlock
)
}
}

override fun onBindViewHolder(holder: VH, position: Int) {
holder.onBind(getItem(position))
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is CommentItem.Level1 -> TYPE_LEVEL1
is CommentItem.Level2 -> TYPE_LEVEL2
is CommentItem.Loading -> TYPE_LOADING
else -> TYPE_FOLDING
}
}

companion object {
private const val TYPE_LEVEL1 = 0
private const val TYPE_LEVEL2 = 1
private const val TYPE_FOLDING = 2
private const val TYPE_LOADING = 3
}
}

可以看到,就是一个简单的多ItemTypeAdapter,唯一需要注意的就是,在Activity里传入的reduceBlock: Reducer.() -> Unit,也要传给每个ViewHolder


ViewHolder


篇幅原因,就只贴其中一个:


abstract class VH(itemView: View, protected val reduceBlock: Reducer.() -> Unit) :
ViewHolder(itemView) {
abstract fun onBind(item: CommentItem)
}

class Level1VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) {
private val avatar: TextView = itemView.findViewById(R.id.avatar)
private val username: TextView = itemView.findViewById(R.id.username)
private val content: TextView = itemView.findViewById(R.id.content)
private val reply: TextView = itemView.findViewById(R.id.reply)
override fun onBind(item: CommentItem) {
avatar.text = item.userName.subSequence(0, 1)
username.text = item.userName
content.text = item.content
reply.setOnClickListener {
reduceBlock.invoke(ReplyReducer(item, itemView.context))
}
}
}

也是很简单,唯一特别一点的处理,就是在onClickListener中,让reduceBlockinvoke一个Reducer实现。


Reducer


刚才在技术选型章节,已经提前展示了“回复评论”这一操作的Reducer实现了,其他Reducer也差不多,比如展开评论操作,也封装在一个Reducer实现ExpandReducer中,以下是完整代码:


data class ExpandReducer(
val folding: CommentItem.Folding,
) : Reducer {
private val mapper by lazy { Entity2ItemMapper() }
override val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {
val foldingIndex = indexOf(folding)
val loaded =
FakeApi.getLevel2Comments(folding.parentId, folding.page, folding.pageSize).getOrNull()
?.map(mapper::invoke) ?: emptyList()
toMutableList().apply {
addAll(foldingIndex, loaded)
}.map {
if (it is CommentItem.Folding && it == folding) {
val state =
if (it.page > 5) CommentItem.Folding.State.LOADED_ALL else CommentItem.Folding.State.IDLE
it.copy(page = it.page + 1, state = state)
} else {
it
}
}
}

}

短短一段代码,我们做了这些事:



  • 请求网络数据Entity list(假数据)

  • 通过mapper转换成显示用的Item数据list

  • Item数据插入到“展开更多”按钮前面

  • 最后,根据二级评论加载是否完成,将“展开更多”的状态置为IDLELOADED_ALL


一个字:丝滑!


用于转换EntityItemmapper的代码也贴一下吧:


// 抽象
typealias Mapper<I, O> = (I) -> O
// 实现
class Entity2ItemMapper : Mapper<ICommentEntity, CommentItem> {
override fun invoke(entity: ICommentEntity): CommentItem {
return when (entity) {
is CommentLevel1 -> {
CommentItem.Level1(
id = entity.id,
content = entity.content,
userId = entity.userId,
userName = entity.userName,
level2Count = entity.level2Count,
)
}

is CommentLevel2 -> {
CommentItem.Level2(
id = entity.id,
content = if (entity.hot) entity.content.makeHot() else entity.content,
userId = entity.userId,
userName = entity.userName,
parentId = entity.parentId,
)
}

else -> {
throw IllegalArgumentException("not implemented entity: $entity")
}
}
}
}

细心的朋友可以看到,在这里我顺便也将热评也处理了:


if (entity.hot) entity.content.makeHot() else entity.content

makeHot()就是用buildSpannedString来实现的:


fun CharSequence.makeHot(): CharSequence {
return buildSpannedString {
color(Color.RED) {
append("热评 ")
}
append(this@makeHot)
}
}

这里可以提一句:尽量用CharSequence来抽象表示字符串,可以方便我们灵活地使用Span来减少UI代码。


data class


也贴一下相关的数据实体得了。



  • 网络数据(假数据)


interface ICommentEntity {
val id: Int
val content: CharSequence
val userId: Int
val userName: CharSequence
}

data class CommentLevel1(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val level2Count: Int,
) : ICommentEntity


  • RecyclerView Item数据


sealed interface CommentItem {
val id: Int
val content: CharSequence
val userId: Int
val userName: CharSequence

data class Loading(
val page: Int = 0,
val state: State = State.LOADING
) : CommentItem {
override val id: Int=0
override val content: CharSequence
get() = when(state) {
State.LOADED_ALL -> "全部加载"
else -> "加载中..."
}
override val userId: Int=0
override val userName: CharSequence=""

enum class State {
IDLE, LOADING, LOADED_ALL
}
}

data class Level1(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val level2Count: Int,
) : CommentItem

data class Level2(
override val id: Int,
override val content: CharSequence,
override val userId: Int,
override val userName: CharSequence,
val parentId: Int,
) : CommentItem

data class Folding(
val parentId: Int,
val page: Int = 1,
val pageSize: Int = 3,
val state: State = State.IDLE
) : CommentItem {
override val id: Int
get() = hashCode()
override val content: CharSequence
get() = when {
page <= 1 -> "展开20条回复"
page >= 5 -> ""
else -> "展开更多"
}
override val userId: Int = 0
override val userName: CharSequence = ""

enum class State {
IDLE, LOADING, LOADED_ALL
}
}
}

这部分没啥好说的,可以注意两个点:



  • data class也是可以抽象的。但这边我处理不是很严谨,比如CommentItem我把userIduserName也抽象出来了,其实不应该抽象出来。

  • 在基于Reducer的框架下,最好是把data class的属性都定义为val


结语


更多的代码就不贴了,贴太多影响观感。有兴趣的朋友可以移步源码


总结一下实现心得:



  • 数据驱动UI

  • 对业务的精准抽象

  • 对异步的延伸理解

  • 灵活使用Collection操作符

  • 没有UI和PM,写代码真TM爽!


作者:blackfrog
来源:juejin.cn/post/7276808079143190565
收起阅读 »

2022年三蹦子团队生存指南

概述 2022年,这一年,经历了四川有酷热到河里没有水发电而导致停电的夏天🌞,也面对此时冷冷的冬天❄️。 但更深的体会,是咱们这个五菱宏光🚚一样的团队,在上半年时,修修补补还能上秋名山一战,大叫:“输者留下车标”。 结果,下半年校招的应届生一到岗,再加上突发...
继续阅读 »

概述


2022年,这一年,经历了四川有酷热到河里没有水发电而导致停电的夏天🌞,也面对此时冷冷的冬天❄️。


但更深的体会,是咱们这个五菱宏光🚚一样的团队,在上半年时,修修补补还能上秋名山一战,大叫:“输者留下车标”。


1.PNG


结果,下半年校招的应届生一到岗,再加上突发需求增多,整个团队的状态就从四个轱辘变成三个轱辘了。而这种三个轱辘的车,北京叫它“三蹦子”,要是后面有棚子能坐人的话,在四川这种车也被叫作“火三轮”。


2.PNG


有一段对三蹦子的描述如下:三轮车前部为驾驶位,后部是车厢,厢体一般为金属制半圆形,可以并排乘坐两个人,车厢上可安装防雨篷,后部车厢下面装有弹簧和两个轮子


就只看这个三蹦子的介绍,就知道这东西只有三个轮子,跑起来不那么靠谱。如果一个团队也这样三个轮子运转的话,估计也不那么靠谱了。


团队


为避免对号入座,人物描写有部分润色,非 100% 人物原本特征


观海👨🏻‍🦲


三蹦子团队的 leader 是观海,作为团队 leader,他负责规划开发计划,需求交付迭代这些琐事。


面对团队躺平的、划水的人,他需要经常苦口婆心地给团队讲:《高效沟通的方法》《有计划的安排工作》 以及 《程序员脱发防治》。他还得时不时威胁一下团队各位成员,敲打敲打大家,让大家多发挥一下主观能动性、把各自工作职责内的事尽量做好一点,让各自纸面的 KPI 好看点,但就是这样做,也仅能维持团队的不散架以及保持自身为数不多的头发👨🏻‍🦲。


阿呜🐕


而我,阿呜,在公司里的职称,说起来是高级研发工程师,当然观海也是高工的,只是他是领导,不能一起论。


其实本来我不是高工,但是他们说,每个团队都需要一个兜底的高级程序员。我也不是谦虚,我说你们另请高明吧。他们说组织上已经决定了,你们团队的高工就是你了。于是,我就成为了高工。


我顺便还得兼职团队的系统工程师。原因是负责各个团队的、真正的系统工程师全都跑去给客户写 ppt 了,每个团队自身的需求分析、系统设计的任务都得研发人员自己搞了。


就这样我不仅要完成自己的研发任务,还要作为类似备份人员对团队其他人员的工作兜底,还得参与相关的设计工作。真的是一个人当两个半人用,还只发一个人的工资。给老板点个👍。


鱿鱼🦑


团队里另一位,我的徒弟,鱿鱼,在来我司之前,用了三年时间,干倒闭了三家公司。


作为一名中级研发工程师,他已经成功的从四大天坑之一的环境工程转行进入了计算机行业。甚至我还记得当年面试他时,他说他发誓绝不回去干环境,要一直做一名研发。


鱿鱼,他为人勤奋,但缺少足够的研发经验,一个原因是非科班欠缺了很多基础知识,二个原因是在之前的公司做研发时,他只接触了非常简单的 CRUD,甚至没有接触过如何做需求分析和设计。这导致很多时候,我把评审过的详细设计文档交到他手上时,他依然会有很多疑问,严重拖慢了自己的开发进度⏰。


年轻人👦🏻


而观海的徒弟,年轻人,作为一名入职一年的助理研发工程师,为人聪明,计算机专业知识扎实,虽然还没有丰富的软件设计经验,但编码经验已经能应对80%的情况了,是团队里安排工作时唯一能让观海放心的人。


他目前最让人津津乐道的成绩是在实习三个月期间,连续拿到了三个优秀评价,而他能拿到这个成绩的原因也在于入职第一周,就在对 ELK 没有任何相关经验的基础上,凭测试提供的些微线索,成功定位并解决了组件升级导致的日志采集策略差异问题。


自此,年轻人在整个部门一炮而红,接着在试用期的三个月里连续解决多个问题单,迅速上手了工作。


时间最终会让他成为一名优秀的工程师。


三板斧🪓🪓🪓


团队里的反面人物,三板斧,一位干了八年研发的中级研发工程师。为何有八年的研发经验、入职我司两年了还是中级呢?


因为他的工作态度让部门老大不同意他的晋升。三板斧,不论是做需求还是改问题单,他上来不做任何分析沟通,直接就对其他研发测试同事连续三个输出:“这个以前就是这样设计的。”🪓“这个不是我写的。”🪓“为什么你认为是问题?”🪓


就这一气呵成的输出,经常气得测试找他们老大过来投诉。


而他做需求开发的话,如果他能拖到月底最后一刻交付代码,那他就绝对不会从月初开始实现需求。而另一方面,面对客户去解决客户问题时,他对客户真的是贴心的“有问必答”,主动告诉客户这就是个 bug,甚至给客户深入讲解设计缺陷,导致客户听完后若有所思,转身就给公司发起一个产品问题投诉。于是整个部门收到的产品投诉,这一年属于三板斧“创造”的投诉能占到 40% 到 50%,部门老大看到他时,都经常玩变脸🌚。


皇上👑


今年下半年入职的应届生,皇上,00 后应届生。他有着典型 00 后的特点,有自己的想法,不顺从“权威”,但也有着小年轻特有的缺点,缺乏用于工作的方式方法。


领导把他安排给我带,我一看,这不活脱脱就是《甄嬛传》里的皇帝吗?👑 皇上是计算机专业毕业的,属于难得的科班,标准的根正苗红。但是我总觉得他大学里是水货,要不然也写不出如下的怪异代码。


public boolean test(boolean flag){
……
if(flag){
return true;
}else if(!flag){
return false;
}
}

问题❓


这样的团队有什么问题,我总结了一下比较核心的几个问题:




  • 问题1:团队人员有人存在明显短板,有人总是躺平,有人总给团队捅娄子




  • 问题2:专职人员缺失(比如系统设计师),导致研发人员的工作职责混乱且效率低下




  • 问题3:新人融入周期长,适应能力、学习能力差,拖累团队整体效率




这些问题对这个三蹦子团队来说是致命的💀。那么最终团队又是怎么没散架,而生存了下来呢?


生存指南📖


首先,针对个别人躺平或者捅娄子的情况,观海在每月任务规划时,就让我提前评估是否可以兜底相关任务,不能兜底的就考虑延期或者拆分需求。


在开发周期中,通过每日晨会对团队成员进度进行评审,并按照检视时间点定时检视成员的当前成果,避免有人真的划水、躺平。


如果在这种情况下,依然出现超进度风险,就由我或者其他成员接手兜底,避免无法交付的风险,同时给予当期划水、躺平的人员较差的考评。


针对专职人员缺失的问题,一方面面对现实,由团队自我协助,通力完成设计,以达成交付。同时让团队研发人员分别主导不同的需求的设计,其他人员辅助参与,锻炼研发人员设计能力;另一方面,在核心需求的开发中,观海会向领导要求系统设计师必须回归本职参与设计。


而新人的培养,则按预估新人能力不足的情况,将其定位为完全不懂计算机的人,让新人主要参与非困难问题单的定位与修改,以达到熟悉项目代码、学习优秀代码、学会计算机逻辑思维的目的。同时辅以试用期考评成绩进行鞭策,推动新人尽快完成从学生到职场打工人的蜕变。


以上措施,虽然也是螺蛳壳里做道场,不能真的解决问题,但尽量公平的让团队成员各司其职,顺便再化阻力为助力,锻炼了团队成员的能力。


但是到了年底,我终于还是忍不住,给观海提了一个一直想问的问题:“像三板斧这样的人,一直这要划水,真的行吗?”


观海给我一个神秘的微笑😄,道:“团队有这样的人存在,不就能证明其他人工作的成绩吗?只有对比才能看出我们工作的价值呀。”


说完,他拍拍我的肩膀,顶着地中海飘然远去,离我在原地久久思考……🤔


作者:阿呜的边城
来源:juejin.cn/post/7175410777447202874
收起阅读 »

开发一个网站,用户密码你打算怎么存储

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。 1....
继续阅读 »

我们在开发网站或者APP时,首先要解决的问题,就是如何安全地传输和存储用户的密码。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。


image.png


1. 如何安全地传输用户的密码


要拒绝用户密码在网络上裸奔,我们很容易就想到使用https协议,那先来回顾下https相关知识吧~


1.1 https 协议


image.png



  • 「http的三大风险」


为什么要使用https协议呢?http它不香吗? 因为http是明文信息传输的。如果在茫茫的网络海洋,使用http协议,有以下三大风险:




  • 窃听/嗅探风险:第三方可以截获通信数据。

  • 数据篡改风险:第三方获取到通信数据后,会进行恶意修改。

  • 身份伪造风险:第三方可以冒充他人身份参与通信。



如果传输不重要的信息还好,但是传输用户密码这些敏感信息,那可不得了。所以一般都要使用https协议传输用户密码信息。



  • 「https 原理」


https原理是什么呢?为什么它能解决http的三大风险呢?



https = http + SSL/TLS, SSL/TLS 是传输层加密协议,它提供内容加密、身份认证、数据完整性校验,以解决数据传输的安全性问题。



为了加深https原理的理解,我们一起复习一下 一次完整https的请求流程吧~


image.png




  1. 客户端发起https请求

  2. 服务器必须要有一套数字证书,可以自己制作,也可以向权威机构申请。这套证书其实就是一对公私钥。

  3. 服务器将自己的数字证书(含有公钥、证书的颁发机构等)发送给客户端。

  4. 客户端收到服务器端的数字证书之后,会对其进行验证,主要验证公钥是否有效,比如颁发机构,过期时间等等。如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密算法的密钥,其实是一个随机值),并且用证书的公钥对这个随机值加密。

  5. 客户端会发起https中的第二个请求,将加密之后的客户端密钥(随机值)发送给服务器。

  6. 服务器接收到客户端发来的密钥之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。

  7. 服务器将加密后的密文返回给客户端。

  8. 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。




  • 「https一定安全吗?」


https的数据传输过程,数据都是密文的,那么,使用了https协议传输密码信息,一定是安全的吗?其实不然




  • 比如,https 完全就是建立在证书可信的基础上的呢。但是如果遇到中间人伪造证书,一旦客户端通过验证,安全性顿时就没了哦!平时各种钓鱼不可描述的网站,很可能就是黑客在诱导用户安装它们的伪造证书!

  • 通过伪造证书,https也是可能被抓包的哦。



1.2 对称加密算法


既然使用了https协议传输用户密码,还是 「不一定安全」,那么,我们就给用户密码 「加密再传输」 呗~


加密算法有 「对称加密」「非对称加密」 两大类。用哪种类型的加密算法 「靠谱」 呢?



对称加密:加密和解密使用 「相同密钥」 的加密算法。



image.png
常用的对称加密算法主要有以下几种哈:


image.png
如果使用对称加密算法,需要考虑 「密钥如何给到对方」 ,如果密钥还是网络传输给对方,传输过程,被中间人拿到的话,也是有风险的哦。


1.3 非对称加密算法


再考虑一下非对称加密算法呢?



「非对称加密:」 非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。



image.png


常用的非对称加密算法主要有以下几种哈:


image.png



如果使用非对称加密算法,也需要考虑 「密钥公钥如何给到对方」 ,如果公钥还是网络传输给对方,传输过程,被中间人拿到的话,会有什么问题呢?「他们是不是可以伪造公钥,把伪造的公钥给客户端,然后,用自己的私钥等公钥加密的数据过来?」 大家可以思考下这个问题哈~



我们直接 「登录一下百度」 ,抓下接口请求,验证一发大厂是怎么加密的。可以发现有获取公钥接口,如下:


image.png
再看下登录接口,发现就是RSA算法,RSA就是 「非对称加密算法」 。其实百度前端是用了JavaScript库 「jsencrypt」 ,在github的star还挺多的。


image.png
因此,我们可以用 「https + 非对称加密算法(如RSA)」 传输用户密码~


2. 如何安全地存储你的密码?


假设密码已经安全到达服务端啦,那么,如何存储用户的密码呢?一定不能明文存储密码到数据库哦!可以用 「哈希摘要算法加密密码」 ,再保存到数据库。



哈希摘要算法:只能从明文生成一个对应的哈希值,不能反过来根据哈希值得到对应的明文。



2.1  MD5摘要算法保护你的密码


MD5 是一种非常经典的哈希摘要算法,被广泛应用于数据完整性校验、数据(消息)摘要、数据加密等。但是仅仅使用 MD5 对密码进行摘要,并不安全。我们看个例子,如下:


public class MD5Test {  
    public static void main(String[] args) {
        String password = "abc123456";
        System.out.println(DigestUtils.md5Hex(password));
    }
}

运行结果:
0659c7992e268962384eb17fafe88364


在MD5免费破解网站一输入,马上就可以看到原密码了。。。


image.png
试想一下,如果黑客构建一个超大的数据库,把所有20位数字以内的数字和字母组合的密码全部计算MD5哈希值出来,并且把密码和它们对应的哈希值存到里面去(这就是 「彩虹表」 )。在破解密码的时候,只需要查一下这个彩虹表就完事了。所以 「单单MD5对密码取哈希值存储」 ,已经不安全啦~


2.2  MD5+盐摘要算法保护用户的密码


那么,为什么不试一下MD5+盐呢?什么是 「加盐」



在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。



用户密码+盐之后,进行哈希散列,再保存到数据库。这样可以有效应对彩虹表破解法。但是呢,使用加盐,需要注意一下几点:




  • 不能在代码中写死盐,且盐需要有一定的长度(盐写死太简单的话,黑客可能注册几个账号反推出来)

  • 每一个密码都有独立的盐,并且盐要长一点,比如超过 20 位。(盐太短,加上原始密码太短,容易破解)

  • 最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。



2.3 提升密码存储安全的利器登场,Bcrypt


即使是加了盐,密码仍有可能被暴力破解。因此,我们可以采取更 「慢一点」 的算法,让黑客破解密码付出更大的代价,甚至迫使他们放弃。提升密码存储安全的利器~Bcrypt,可以闪亮登场啦。



实际上,Spring Security 已经废弃了 MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt 生而为保存密码设计的算法,相比 MD5 要慢很多。



看个例子对比一下吧:


public class BCryptTest {  

    public static void main(String[] args) {
        String password = "123456";
        long md5Begin = System.currentTimeMillis();
        DigestUtils.md5Hex(password);
        long md5End = System.currentTimeMillis();
        System.out.println("md5 time:"+(md5End - md5Begin));
        long bcrytBegin = System.currentTimeMillis();
        BCrypt.hashpw(password, BCrypt.gensalt(10));
        long bcrytEnd = System.currentTimeMillis();
        System.out.println("bcrypt Time:" + (bcrytEnd- bcrytBegin));
    }
}

运行结果:


md5 time:47


bcrypt Time:1597


粗略对比发现,BCrypt比MD5慢几十倍,黑客想暴力破解的话,就需要花费几十倍的代价。因此一般情况,建议使用Bcrypt来存储用户的密码


3. 总结



  • 因此,一般使用https 协议 + 非对称加密算法(如RSA)来传输用户密码,为了更加安全,可以在前端构造一下随机因子哦。

  • 使用BCrypt + 盐存储用户密码。

  • 在感知到暴力破解危害的时候,「开启短信验证、图形验证码、账号暂时锁定」 等防御机制来抵御暴力破解。


作者:小王和八蛋
来源:juejin.cn/post/7260140790546251831
收起阅读 »

2022被裁员两次的应届毕业生的年终总结

前言 “生活的苦可以被疲劳麻痹、被娱乐转移,最终变得习以为常、得过且过,可以称之为钝化。学习的苦在于,始终要保持敏锐而清醒的认知,乃至丰沛的感情,这不妨叫锐化。” 1. 二月,初到上海 1.1 第一段艰辛的实习生涯 从学校到校园,仿佛好像是一瞬之间。现在回想...
继续阅读 »

前言


“生活的苦可以被疲劳麻痹、被娱乐转移,最终变得习以为常、得过且过,可以称之为钝化。学习的苦在于,始终要保持敏锐而清醒的认知,乃至丰沛的感情,这不妨叫锐化。”



1. 二月,初到上海


1.1 第一段艰辛的实习生涯


从学校到校园,仿佛好像是一瞬之间。现在回想起21年的秋招,也算是收获满满,拿下了不少大大小小好几家公司的offer。那时候对于面试题和自己的项目都有较为深刻的印象,由于本人表达能力还不错(面试一堆胡吹),经过五轮面试最终接下了壹药网的offer。 仿佛一切美好都在向我招手,世间是如此的美好。


image.png在2.17入职之后,也算是正式开启了社畜的角色。奈何在公司工作不到半个月之后,开始迎来了为期三个多月的疫情,疫情不仅是对公司有着强烈的冲击,对打工人也是晴天霹雳。


由于是第一次实习,Git工具根本就不会用 (此时省略一万点艰辛,以致于我们老大叫带我的导师,专门给我先培训好我的Git技能,在这里也是超级感谢我的导师,在远程办公 事务繁忙 我还贼菜的情况下,历时一个多月我的GIT终于出师)。

不过在实习期间,也学到了很多中型的公司的开发流程以及代码规范等等,也是宝贵的实习经历让我逐渐过渡到一个标准的社畜。


2.六月,第一次被裁员


在疫情解封的第一周的第三天下午,领导把我叫到会议室,通知所有的校招生全部解约。那时已是6.13号,校招已经结束,并且我已经答辩结束顺利 “毕业” 了。此时陷入了非常被动的局势,校招已经过了时间,社招没有工作经验。
那时候让我真真切切感受到互联网公司的不稳定,也让我感受到找工作的不容易。此时我也是被迫开始了海投模式,每天都在刷BOSS直聘,每天都在EMO ,并且面试题根本看不进去啊,谁能懂?

此时逃离上海成为了我最大的想法,奈何疫情当下,去哪里都要隔离 并且杭州的公司是一家回应的都没有,此时我内心是奔腾的。 有那种陷入谷底的绝望(没敢和家里面人说,只能自己硬抗)


2.1 试用期两个月,正式工两个月


肯定是上天眷顾我,觉得我自己硬扛着太不容易了。给我了个机会,在海投十天之后,那天上午突然一个电话打给我,问我下午有没有时间面试,此时我内心的感觉(只要你们愿意要我,我愿意当牛做马,工资啥的都无所谓,主要是给老板打工)。 当时也算是比较幸运,在我的再一次胡说海吹之下,拿到了第二家公司的offer。 试用期两个月,工资打八折。这家公司入职之后,公司全是年轻人,技术用的也很新,主要是都是河南人 真的亲切啊。我也是很快就融入了公司的氛围里面,开始称兄道弟的。两个月后在我的班门弄斧之下,顺利转正了,虽然自己陆陆续续也弄出了好几个线上较为严重的BUG 但还是在大家的努力下成功补救了回来。超级感谢当时公司里面的雷哥,权哥,昊哥等等,帮我帮了超级多。同时也督促我要一直看书一直学习来着。


xuexi.png


于是乎,周六周日有时间都会去公司熟悉业务,精进自己的代码能力。



早上上班拍的公司照片,真的超级好看鸭。


3.十一月,第二次被裁员


就这样在公司一直干着,经常会加班(1.5倍的加班费,真的超级香),可是后来也陆陆续续有些消息说公司业绩不太行,疫情(再一次给我送来了惊喜),然后11.25号又被老大 再一次叫到了办公室里面,开门见山,立马滚蛋。

就是如此狗血,就是这么残忍。我现在依稀记得,就在上周我又弄出了一个超级大的BUG,导致业务受到了极大的影响。

业务改版,对之前老的数据迁移有问题,并且新的数据也有部分问题(还是太不认真,太年轻了),导致投诉电话不断,产品直接都要崩溃。没办法,又有好多人给我擦屁股。然后第二周老板宣布裁员,我和一个前端都被开除了。那天上海降温超级明显,并且还下着小雨和我的心情是一样一样的。


dierci.jpg


那天拍的最后一次公司的图片


2.十二月,开启第三份工作


在第二次被裁员之后,我是真的对自己产生了深深地怀疑,也觉得为什么我一个应届毕业生要被裁员两次。不得不否认,我的技术水平是真的菜,代码水平也是真的烂,运气也真的好差劲。

对啊,为什么幸福不是我,我没有乱七八糟的圈子,不出去乱玩每天不是上班就是下班,下班就回去煮饭吃,看看书就睡觉,周末休息就回家,我不明白生活为什么要给予我如此重重的打击,可是生活总得继续下去,我也只能收拾好行李,再出发。
不过还好,在我摆烂了大概几天之后,我又开始再一次的海投模式 同样收到的回复很少,很少有需要2022届毕业生的,简历都不太好包装。好像上帝给我关了一扇门,总会给我开一扇窗。那个本来可以不认真对待的面试题,在我认认真真对待之后,成功收到了一面通知,然后线下的面试(我不得不承认有被打击到,但是我的胡说海吹的功夫也不是盖得),最终成功拿下了两家公司的offer(另一家没有细讲,因为没去,为什么没去,钱没给到位))。


zijie.jpg


面试路上,路过字节


2023年一月,找到对象


哈哈哈哈哈哈哈哈哈哈哈嗝,虽然2022年职场过得比较坎坷。但是我想告诉大家的是,大年初一我就遇到了我对象。哈哈哈哈哈哈哈哈哈哈嗝。她真的超级超级好,我也超级超级喜欢她。2023除了升职加薪,那就是好好爱她,带她吃好多好吃的,玩好多好玩的。

送大家一句话: 没娶的别慌,待嫁的别忙, 经营好自己,珍惜当下时光。一切该来的总会到。 怕什么,岁月漫长,你心地善良 终会有一人陪你骑马喝酒走四方


seeMovie.png


一起看的第一场电影


作者:厦天的梦想
来源:juejin.cn/post/7197411581927833655
收起阅读 »

回看 2023雷军年度演讲·《成长》

一、武大往事 1)武大学习 1987 年考上了武汉大学,武大图书馆看了一本书《硅谷之火》,奠定了一生的 梦想。创办一家伟大的公司,梦想之火彻底点燃。 大一新生,目标从想创办伟大的公司,到把书念好的正事思想观念的转变。如何把书读的不同凡响?确定一个两...
继续阅读 »

一、武大往事


武大往事.png


1)武大学习


硅谷之火.png



  • 1987 年考上了武汉大学,武大图书馆看了一本书《硅谷之火》,奠定了一生的 梦想。创办一家伟大的公司,梦想之火彻底点燃。


大一新生.png



  • 大一新生,目标从想创办伟大的公司,到把书念好的正事思想观念的转变。如何把书读的不同凡响?确定一个两年修改完大学四年的所有学分地域模式的目标。解决问题:

    • 学分制,如何选课?

    • 积极主动,找老乡学长请教。

    • 遇到的问题如何解决(认知:不是独一无二,绝大部分别人都遇到了,别人都解决了,你所需要做的就是找个懂的人问一问(这个能解决大问题))。




标准答案.png


2)如何搞定自学?



  • 课程难度大:数学系分离的计算机系,全部是数学专业数学,难度大。

  • 开窍(往下读,遇到看不懂的先跳过去),知识点不是绝对线性的。

  • 自学中寻找各种方法,用不同的方法处理自己学习中遇到的问题。

  • 当今社会瞬息万变,自己从事的职业可能不是自己所学的专业,相差很多。


两年修完所有学分.png



学习能力是最重要的能力,一定要掌握各种各样的学习方法,同时要养成终生学习的习惯,你才足以面未来所有的挑战。



3)搞定点名



  • 与高年级一起上,两倍的课程的难度,可能互相冲突。分析点名的目的,解决实际问题:加深老师对自己的印象,形成一个好的印象,之后点名缺课,就不是问题了。

  • 克服了重重困难,终于两年搞定大学的所有的学分。


二、第二个目标:成为一个优秀的程序员


成为优秀的程序员.png


上机资源少,只有一台 68000 的小型机。每周只有两次机会练习,每次只有两个小时。如何获取更多的计算机上机机会?


汇编代码.png



  • 寻找和获取额外的上机实践机会。

  • 在纸上练习打字键盘,提升自己的打字能力。

  • 在纸上写程序,写到像教材的示范程序一样。

  • 第一门编程语言《Pascal 语言编程设计》,并成功进入新版教材。

  • 进入老师实验室做课题,有更多机会使用计算机,最终修成了技术高手。

  • 参与内存管理开源工具开发,并在圈内广受欢迎。


三、第三个目标: 在学报上发论文


发论文.png
本科生发论文困难。90年度初,国内计算机病毒第一次大爆发。得到两位老师的帮助,写了第一波的杀毒软件,将这些内容整理出了论文,1992年8月《计算机研究与发展》 被录用了和发表。


敢想敢干.png



年轻人还是要敢想敢干... 一往无前,先干了再说,也许没有想象中那么难。



四、梦想与成长


梦想与成长.png



梦想被点燃了,我幸运的是真的把梦想当回事,并且学会了拆解成一个有一个的目标,然后竭尽全力去完成。



五、创业之旅


创业之旅.png
武汉电子一条街,从小企业蹭最新的电脑,到得到技术上的认可。到写加密软件。同时两台电脑的 一台写程序,一套跑测试(因为两台电脑,可以用 快乐加倍)。 15 天高强度开发,输出了 BITLOK 商业加密软件,1989 年 8 月发布,因为在圈内获得不少的关注买了很年,也因此获得了不少的收入。



一个人的能力再强,也是有限的,找互补的朋友一起干,更容易成功。



六、创业


80年末网络设备公司.png


创办三色公司(三色计算机部),包含四个创始人,每个人股份占比(25%)。 出现了管理上问题:



第一个问题:谁说了算?



1)创业维艰



  • 大学开始创业,开始解决问题,并获得了一些收获

  • 创业艰难的小故事

  • 创业失败,回归学校

  • 复盘问题



创办公司太不容易,光有技术是不够的,还有太多的东西要学。



2)创业与成长


创业.png


2.1) 金山


快与远.png
金山内部, 35 年来最重要的经验:



一个人可能走的更快,但一群人走,才能走的更远。



也正是如此完成了,从程序员到管理者的蜕变;


2.2)创办小米


改变中国制造业,创新的商业模式,梦想逐步实现。



回望三十多年的创业之旅,就是不断追寻梦想并不断成长的过程。



七、高端探索


高端探索.png


争议问题:



  1. 做高端太难了,能不能不做?

  2. 用小米品牌能做成高端吗?



高端是小米发展的必由之路,更是生死之战。



影像探索



  • dxo 评测第一,但是用户不满意

  • 徕卡老师,与徕卡合作

  • 摄影文化、人文摄影

  • 小米 13 小屏探索


八、认知与成长


认知的突破.png



只有认知的突破,才能带来真正的成长。




每一段经历,每一次蜕变,都是一次认知的突破,更是一次关键的成长。



九、在时代变迁的洪流中,怎么才能保持内心的平静呢?


成长.png



  • 成长!



只有脚踏实地的成长,才有足够的自信、勇气与决心,去迎接所有未知的挑战;只有脚踏实地的成长,才能让自己内心充实,眼里有光,时时刻刻充满力量。




  • 人生是一场马拉松,一时的成败得失,都不那么重要。

  • 所有人生难题,都将在成长中找到答案。

  • 人因梦想而伟大,又因坚持梦想而成长!


作者:进二开物
来源:juejin.cn/post/7276674188215386152
收起阅读 »

Android:实现一个简单带动画的展开收起功能

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 今天给大家带来一个展开和收起的简单效果。如果只是代码中简单设置显示或隐藏,熟悉安卓系统的朋友都知道,那一定是闪现。所以笔者结合了动画,使得体验效果瞬间提升一个档次。话不多说,直接上效...
继续阅读 »

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。



今天给大家带来一个展开和收起的简单效果。如果只是代码中简单设置显示或隐藏,熟悉安卓系统的朋友都知道,那一定是闪现。所以笔者结合了动画,使得体验效果瞬间提升一个档次。话不多说,直接上效果:


1.gif


首先观察图中效果,视图有展开和折叠两种状态,右侧图标和文字会跟随这个状态改变。那么其中就有折叠的高度和展开的高度需要我们记录。折叠高度是固定的,展开高度需要动态获取。需要注意的是不能直接通过视图直接获取高度,因为视图的绘制和Activity的生命周期是不同步的,在Activity中直接lin.height获取高度无法保证此时的视图已经完成计算。这里直接用简单的post方式获取到绘制完成的总高度。原理是将这个消息放到队列最后一条,这样就可以保证回调方法中能够获取到真实的高度。


lin?.post {
val h = lin!!.height
hight = if (h > 0) h else baseHight

if (h > 0 && ivTop?.visibility == View.GONE) {
ivTop?.visibility = View.VISIBLE
}
}

接下来就是动画的使用和动态控制视图的高度了。这里需要用到属性动画,我们知道的属性动画有ValueAnimatorObjectAnimatorObjectAnimator是继承于ValueAnimator,说明ValueAnimator能做的事情ObjectAnimator也可以实现。由于我们要控制的视图不止一个,所以还是使用ValueAnimator方便点。通过addUpdateListener添加监听后,animation.animatedValue就是我们需要的当前值。在此处不停将当前高度赋值给视图,并且图标也根据这个值进行等比例的旋转以到达到视图不停更新。


//根据展开、关闭状态传入对应高度
val animator = ValueAnimator.ofInt(
if (isExpand) hight - baseHight else 0,
if (isExpand) 0 else hight - baseHight
)
animator.addUpdateListener { animation ->
val params = lin?.layoutParams
params?.height = if ((animation.animatedValue as Int) < baseHight) baseHight else (animation.animatedValue as Int) //当高度小于基础高度时 给与基础高度
lin?.layoutParams = params//拿到当前高度
//图标旋转
ivTop?.rotation = (animation.animatedValue as Int) * 180f / (hight - baseHight)

}
animator.duration = 500//动画时长
animator.start()

isExpand = !isExpand
tvExpand?.text = if (isExpand) "关闭" else "展开"

编写过程需要注意展开和收起状态下值的正确输入,在回调方法中获取对应的当前值并赋值。


好了,一个简单的展开收起功能就实现了,希望对大家有所帮助。


作者:似曾相识2022
来源:juejin.cn/post/7273079438991376439
收起阅读 »

韩国程序员面试考什么?

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。 韩国的面试都考什么?有没有国内的卷呢? 可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。 再看看贡献者,嗯,...
继续阅读 »

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。


韩国的面试都考什么?有没有国内的卷呢?
瘦巴巴的老爷们


可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。


star


再看看贡献者,嗯,明显看出来是韩国人。
贡献者


整体看一下内容。


第一大部分是计算机科学,有这些小类:



  • 计算机组成


计算机组成原理



  • 数据结构


数据结构




  • 数据库
    数据库




  • 网络




网络



  • 操作系统


操作系统


软件工程


先不说内容,韩文看起来也够呛,但是基础这一块,内容结构还是比较完整的。


第二大部分是算法:
算法


十大排序、二分查找、DFS\BFS…… 大概也是那些东西。


第三大部分是设计模式,内容不多。
设计模式


第四大部分是面试题:
面试题


终于到了比较感兴趣的部分了,点进语言部分,进去看看韩国人面试都问什么,随便抽几道看看:
面试题



  • Vector和ArrayList的区别?

  • 值传递 vs 引用传递?

  • 进程和线程的区别?

  • 死锁的四个条件是什么?

  • 页面置换算法?

  • 数据库是无状态的吗?

  • oracle和mysql的区别?

  • 说说数据库的索引?

  • OSI7层体系结构?

  • http和https的区别是?

  • DI(Dependency Injection)?

  • AOP(Aspect Oriented Programming)?

  • ……


定睛一看,有种熟悉的感觉,天下八股都一样么?


第五大部分是编程语言:
编程语言


包含了C、C++、Java、JavaScript、Python。


稍微看看Java部分,也很熟悉的感觉:



  • Java编译过程

  • 值传递 vs 引用传递

  • String & StringBuffer & StringBuilder

  • Thread使用


还有其它的Web、Linux、新技术部分就懒得再一一列出了,大家可以自己去看。


这个仓库,让我来评价评价,好,但不是特别好,为什么呢?大家可以看看国内类似的知识仓库,比如JavaGuide,那家伙,内容丰富的!和国内的相比,这个仓库还是单薄了一些——当然也可能是韩国的IT环境没那么卷,这些就够用了。


再扯点有点没的,我对韩国的IT稍微有一点点了解,通过Kakao。之前对接过Kakao的支付——Kakao是什么呢?大家可以理解为韩国的微信就行了,怎么说呢,有点离谱,他们的支付每天大概九点多到十点多要停服维护,你能想象微信支付每天有一个小时不可用吗?


也有同事对接过Kakao的登录,很简单的一个Oauth2,预估两三天搞定,结果也是各种状况,搞了差不多两周。


可能韩国的IT环境真的没有那么卷吧!


有没有对韩国IT行业、IT面试有更多了解的读者朋友呢?欢迎和老三交流。



对了,仓库地址是:github.com/gyoogle/tec…



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

帮你省时间,看看 bun v1.0 怎么用!

web
本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档 一、bun v1.0 做了什么? all in JavaScript/TypeScript app。看起真的很了不起! 作为JS/TS运行时 作为包管理工具和包运行...
继续阅读 »

本文基于 Window Ubuntu WSL 环境测试,本文只选取重点,细节需查看文档



一、bun v1.0 做了什么?



all in JavaScript/TypeScript app。看起真的很了不起!




  • 作为JS/TS运行时

  • 作为包管理工具和包运行器

  • 作为构建工具

  • 作为测试运行器

  • 对外提供 API


资源



二、安装 bun v1.0



bun 目前不支持 window 环境,但是可以在 wsl 中使用。



2.1) 各种安装方法



  • curl


curl -fsSL https://bun.sh/install | bash # 检查:which bun


  • 使用 npm 安装


npm install -g bun # 检查:which bun


  • 其他平台的安装方法



brew tap oven-sh/bun # for macOS and Linux
brew install bun # brew
docker pull oven/bun # docker

2.2) bun 提供的命令


命令描述
init初始化一个 bun 项目
run运行一个文件或者脚本
test运行测试
xbun x 的别名,类似于 npx
repl进入交互式环境
create使用模板创建项目
install安装依赖
add添加依赖
remove移除依赖
update更新依赖
link全局链接一个 npm 包
unlink移除全局链接的 npm 包
pm更多的包管理命令
build打包 TypeScript/JavaScript 文件到单个文件
update获取最新的 bun 版本

三、作为 JS/TS 运行时


bun index.js // 运行 js 文件
bun run index.ts // 运行 ts 文件
// 其他相关的 tsx/jsx/...

如果直接运行 index.tsx 没有任何依赖会报错:


const Ad = <div>ad</div>

console.log(Ad)

// bun index.tsx
// 错误:Cannot find module "react/jsx-dev-runtime" from "/xxx/index.tsx"

四、作为包管理工具和包运行器


4.1)初始化一个项目


bun init # 与 npm init -y 类似

4.2)使用脚手架


# 与 npx 类似, 以下可能常用的初始化项目的脚手架
bun create react-app
bun create remix
bun create next-app
bun create nuxt-app

五、作为构建工具



  • 初始化一个简单的项目


cd your_dir
bun init # 默认
bun add react react-dom # 添加依赖包
touch index.tsx


  • 添加 TSX 文件内容


import React from 'react'

const App = () => {
return <div>This is App</div>
}


  • 打包 bun build


bun build ./index.tsx --outfile=bundle.js


提示:bundle.js 中打包了 react 相关的包。



六、作为测试运行器


测试与 Jest 非常相似, 以下是官方示例:


import { expect, test } from "bun:test";

test("2 + 2", () => {
expect(2 + 2).toBe(4);
});

运行时测试:


bun test

速度很快,输出结果:


bun test v1.0.0 (822a00c4)

t.test.ts:
✓ 2 + 2 [1.03ms]

1 pass
0 fail
1 expect() calls
Ran 1 tests across 1 files. [92.00ms]

七、对外提供 API


项目描述
HTTP 服务处理 HTTP 请求和响应
WebSocket 套接字支持 WebSocket 连接
Workers 工具在后台运行多线程任务
Binary data处理二进制数据
Streams流处理
File I/O文件输入/输出操作
import.meta访问模块元信息
SQLite使用 SQLite 数据库
FileSystemRouter文件系统路由器
TCP socketsTCP 套接字通信
Globals全局变量和对象
Child processes创建子进程
Transpiler转译器
Hashing哈希函数和算法
Console控制台输出
FFI外部函数接口
HTMLRewriterHTML 重写和转换
Testing测试工具和框架
Utils实用工具函数
Node-APINode.js API 访问

八、展望



  • windows 支持


小结


本文主要讲解了 bun v1.0 中所做的事情,包含极速的运行时、一体化的包管理工具、内置测试运行器、构建应用(打包)和对象提供各种类型的 API(兼容 Node API(非完全)),如此功能能完整的 bun 你想尝试一下吗?


作者:进二开物
来源:juejin.cn/post/7277399972916428835
收起阅读 »

百度工程师移动开发避坑指南——Swift语言篇

iOS
上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。 对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易...
继续阅读 »

上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。


对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易在相关代码上踩坑。


本期我们带来与Swift可选类型相关的几个避坑指南:可选类型要判空;避免使用隐式解包可选类型;合理使用Objective-C标识符;谨慎使用强制类型转换。希望能对Swift开发者有所帮助。


一、可选类型(Optional)要判空


在Objective-C中,可以使用nil来表示对象为空,但是使用一个为nil的对象通常是不安全的,如果使用不慎会出现崩溃或者其它异常问题。在Swift中,开发者可以使用可选类型表示变量有值或者没有值,可以更加清晰的表达类型是否可以安全的使用。如果一个变量可能为空,那么在声明时可以使用?来表示,使用前需要进行解包。例如:

var optionalString: String?

在使用可选类型对象时,需要进行解包操作,有两种解包方式:强制解包与可选绑定。


强制解包使用 ! 修饰一个可选对象 ,相当于告诉编译器『我知道这是一个可选类型,但在这里我可以保证他不为空,编译时请忽略此处的可空校验』,例如:

let unwrappedString: String = optionalString!  // 运行时报错:Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

这里使用 ! 进行了强制解包,如果optionalString为nil,将会产生运行时错误,发生崩溃。**因此,在使用 ! 进行强制解包时,必须保证变量不为nil,要对变量进行判空处理,**如下:

if optionalString != nil {
let unwrappedString = optionalString!
}

相较于强制解包的不安全性,一般而言推荐另一种解包方式,即可选绑定。例如:

if let optionalString = optionalString {
// 这里optionalString不为nil,是已经解包后的类型,可以直接使用
}

综上,在对可选类型进行解包时应尽量避免使用强制解包,采用可选绑定替代。如果一定要使用强制解包,那么必须在逻辑上完全保证类型不为空,并且做好注释工作,以增加后续代码的可维护性。


二、避免使用隐式解包可选类型(Implicitly Unwrapped Optionals)


由于可选类型每次使用之前都需要进行显式解包操作,有时变量在第一次赋值之后,就会一直有值,如果每次使用都显式解包,显得繁琐,Swift引入了隐式解包可选类型,隐式解包可选类型可以使用 ! 来表示,并且使用时不需要显式解包,可以直接使用,例如:

var implicitlyUnwrappedOptionalString: String! = "implicitlyUnwrappedOptionalString"
var implicitlyString: String = implicitlyUnwrappedOptionalString

上述例子的隐式解包,在编译和运行过程中都不会发生问题,但如果在两行代码中间插入一行 implicitlyUnwrappedOptionalString = nil将会产生运行时错误,发生崩溃。


在我们实际项目中,一个模块通常由多人维护,通常很难保证变量在第一次赋值之后一直不为nil或者只有在第一次正确赋值之后使用,从安全角度考虑,在使用隐式解包类型之前也要进行判空操作,但这样就和使用可选类型没有区别。对于可选类型(?),不经过解包直接使用编译器会报告错误,对于隐式解包类型,则可直接使用,编译器无法帮助我们做出是否为空的检查。因此,在实际项目中,不推荐使用隐式解包可选类型,如果一个变量是非空的,则选择非空类型,如果不能保证是非空的,则选择使用可选类型。


三、合理使用Objective-C标识符


与Swift不同的是,OC是一种动态类型语言,对于OC而言没有optional这个概念,无法在编译期间检查对象是否可空。苹果在 Xcode 6.3 中引入了一个 Objective-C 的新特性:Nullability Annotations,允许编码时使用nonnull、nullable、null_unspecified等标识符告诉编译器对象是否是可空或者非空的,各标识符含义如下:


nonnull,表示对象是非空的,有__nonnull和_Nonnull等价标识符。


nullable,表示对象可能是空的,有__nullable 和_Nullable等价标识符。


null_unspecified,不知道对象是否为空,有__null_unspecified等价标识符。


OC标识符标注的对象类型和Swift类型对应关系如下:




除了以上标识符外,现在通过Xcode创建的头文件默认被 NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 包住,即在这之间声明的对象默认标识符是 nonnull 的。


在Swift与OC混编场景,编译器会根据OC标识符将OC的对象类型转换成Swift类型,如果没有显式的标识,默认是null_unspecified。例如:

@interface ExampleOCClass : NSObject
// 没有指定标识符,且没有被NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包裹,标识符默认为null_unspecified
+ (ExampleOCClass *)getExampleObject;
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getExampleObject {
return nil; // OC代码直接返回nil
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let _ = ExampleOCClass.getExampleObject().description // 报错:Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
}
}

在上面例子中,Swift代码调用OC接口获取一个对象,编译器隐式的将OC接口返回的对象转换为隐式解包类型来处理。由于隐式解包类型可以不显式解包直接使用,使用者往往会忽略OC返回的是隐式解包类型,不通过判空而直接使用。但当代码执行时,由于OC接口返回了一个nil,导致Swift代码解包失败,发生运行时错误。


在实际编码中,推荐显式指定OC对象为nonnull或者nullable,针对上述代码进行修改后如下:

@interface ExampleOCClass : NSObject
/// 获取可空的对象
+ (nullable ExampleOCClass *)getOptionalExampleObject;
/// 获取不可空的对象
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject;
@end

@implementation ExampleOCClass
+ (ExampleOCClass *)getOptionalExampleObject {
return nil;
}
+ (ExampleOCClass *)getNonOptionalExampleObject {
return [[ExampleOCClass alloc] init];
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 标注nullable后,编译器调用接口时,会强制加上 ?
let _ = ExampleOCClass.getOptionalExampleObject()?.description
// 标注nonnull后,编译器将会把接口返回当做不可空来处理
let _ = ExampleOCClass.getNonOptionalExampleObject().description
}
}

在OC对象加上nonnull或者nullable标识符后,相当于给OC代码增加了类似Swift的『静态类型语言的特性』,使得编译器可以对代码进行可空类型检测,有效的降低了混编时崩溃的风险。但这种『静态特性』并不对OC完全有效,例如以下代码,虽然声明返回类型是nonnull的,但是依然可以返回nil:

@implementation ExampleOCClass
+ (nonnull ExampleOCClass *)getNonOptionalExampleObject {
return nil; // 接口声明不可空,但实际上返回一个空对象,可以通过编译,如果Swift当作非空对象使用,则会发生崩溃
}
@end
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
ExampleOCClass.getNonOptionalExampleObject().description
}
}

基于以上例子,依然会产生运行时错误。从安全性的角度上来说,似乎Swift最好在使用所有OC的接口时都进行判空处理。但实际上这将导致Swift的代码充斥着大量冗余的判空代码,大大降低代码的可维护性,同时也违背了『暴露问题,而非隐藏问题』的编码原则,并不推荐这么做,合理的做法是在OC侧做好安全校验,OC对返回类型应做好检验,保证返回类型的正确性,以及返回值和标识符能够对应。


综合来看,OC侧标识符最好遵循如下使用原则:


1、不推荐使用NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END,因为默认修饰符是nonnull的,在实际开发中很容易忽略返回的对象是否为空。返回空则会导致Swift运行时错误。推荐所有涉及混编的OC接口都需要显式使用相应的标识符修饰。


2、OC接口要谨慎使用 nonnull 修饰 ,必须确保返回值不可能是空的情况下使用,任何不能确定不可空的接口都需要标注为nullable。


3、为避免Swift侧不必要的类型、判空等校验(违背Swift设计理念),在理想状态下需在OC侧进行类型的校验,保证返回对象和标注的标识符完全正确,这样Swift则可以完全信赖OC返回的对象类型。


4、在Swift调用OC代码时,要关注OC返回的类型,尤其是返回隐式解包类型时,要做好判空处理。


5、在OC代码支持Swift调用前,提前对OC代码做好返回类型和标识符的检查,确保返回Swift的对象是安全的。


四、谨慎使用强制类型转换


GEEK TALK


Swift 作为强类型语言,禁止一切默认类型转换,这要求编码者需要明确定义每一个变量的类型,在需要类型转换时必须显式的进行类型转换。Swift可以使用as和as?运算符进行类型转换。


as运算符用于强制类型转换,在类型兼容情况下,可以将一个类型转换为另一个类型,例如:

var d = 3.0 // 默认推断为 Double 类型
var f: Float = 1.0 // 显式指定为 Float 类型
d = f // 编译器将报错“Cannot assign value of type 'Float' to type 'Double'”
d = f as Double // 需要将Float类型转换为Double类型,才能赋值给f

除了以上列举的基本类型外,Swift还兼容基础类型与对应的OC类型的转换,比如NSArray/Array、NSString/String、NSDictionary/Dictionary。


如果类型转换失败,将会导致运行时错误。例如:

let string: Any = "string"
let array = string as Array // 运行时错误

这里string变量实际是一个String类型,尝试将String类型转换成Array类型,将导致运行时错误。


另一种类型转换的方式是使用as?运算符,如果转换成功,返回一个转换类型的可选类型,如果转换失败,返回nil。例如:

let string: Any = "string"
let array = string as? Array // 转换失败,不会产生运行时错误

这里由于无法将String类型转换为Array类型,因此转换失败,array变量的值为nil,但不会产生运行时错误。


综合来看,在进行类型转换时,需要注意以下几点:


1、类型转换只能在兼容的类型之间进行,例如Double和Float可以相互转换,但String和Array之间不能相互转换。


2、如果使用as进行强制类型转换,需要确保转换是安全的,否则将会导致运行时错误。如果不能确保转换类型之间是兼容的,则应该使用as?运算符,例如将网络数据解析成模型数据时,无法保证网络数据的类型,应该使用as?。


3、在使用as?运算符进行类型转换时,需要注意返回值可能为nil的情况。


----------  END  ----------


推荐阅读【技术加油站】系列:


百度工程师移动开发避坑指南——内存泄漏篇


百度程序员开发避坑指南(Go语言篇)


百度程序员开发避坑指南(3)


百度程序员开发避坑指南(移动端篇)


百度程序员开发避坑指南(前端篇)


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

为什么react中的hooks都要放在顶部?

1. 使用场景: 公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了 react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。 2.官网解释: 1.官网截图镇楼: 2.那我写在条件语句中会怎样 ...
继续阅读 »

1. 使用场景:


公司开会的时候有师兄问到为什么hooks定义一般都写在顶部,对于这个问题我以前总结过,这次看了
react新文档后我给出更加详细的解释并给出具体代码来解释为什么要放在顶部。


2.官网解释:


1.官网截图镇楼:




2.那我写在条件语句中会怎样


我给出一段代码:其中const [message, setMessage] = useState('');写在了条件语句里面

import { useState } from 'react';

export default function FeedbackForm() {
const [isSent, setIsSent] = useState(false);
if (isSent) {
return <h1>Thank you!</h1>;
} else {
// eslint-disable-next-line
const [message, setMessage] = useState('');
return (
<form onSubmit={e => {
e.preventDefault();
alert(`Sending: "${message}"`);
setIsSent(true);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<br />
<button type="submit">Send</button>
</form>
);
}
}

效果图:这是一个收集用户反馈的小表单。当反馈被提交时



 它应该显示一条感谢信息,当我点击确定时出现一条错误。




“渲染的 hooks 比预期的少”


3.那我不写在顶部可能会怎样


下方的const [message, setMessage] = useState('');并没有写在顶部

   import { useState } from 'react';

export default function FeedbackForm() {
const [isSent, setIsSent] = useState(false);
if (isSent) {
return <h1>Thank you!</h1>;
}
const [message, setMessage] = useState('');
return (
<form onSubmit={e => {
e.preventDefault();
alert(`Sending: "${message}"`);
setIsSent(true);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<br />
<button type="submit">Send</button>
</form>
);
}
}

效果图:



 点击确认后:
同样出现这个错误:提前return导致后面一个hooks没有渲染。




4.原因分析


从源码的角度来说的话,React会在内部创建一个名为“Hooks”(中文为钩子)的数据结构来追踪每个组件的状态。


在函数组件中调用Hook时,React会根据Hook的类型将其添加到当前组件的Hooks链表中。然后,React会将这些Hooks存储在Fiber节点的“memoizedState”字段中,以便在下一次渲染时使用。


如果你在代码中多次调用同一个Hook,React会根据Hooks的顺序将其添加到当前组件的Hooks链表中。这样,React就可以确定哪个状态应该与哪个组件关联,并且能够正确地更新UI。


以下是一个示例代码片段:

import { useState, useEffect } from 'react';

function useCustomHook() {

const [count, setCount] = useState(0);

useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return [count, setCount];
}

export default function MyComponent() {

const [count, setCount] = useCustomHook();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

在上面的代码中,useCustomHook是一个自定义Hook,它使用useStateuseEffectHook来管理状态。在MyComponent中,我们调用自定义Hook并使用返回值来渲染UI。


由于useCustomHook只能在函数组件或其他自定义Hooks的最顶层调用,我们不能将它嵌套在条件语句、循环或其他函数内部。如果这样做,React将无法正确追踪状态并更新UI,可能导致不可预测的结果。如果我们条件渲染中使用可能导致没有引入useCustomHook(),从而导致错误。


总结描述就是创建了一个链表,当在条件语句中使用hooks时可能会导致前后两次链表不同,从而导致错误,所以我们必须尽可能避免这种错误从而写在顶部。


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

😕😕刚工作三天就被裁是一种怎样的体验

前言 还有谁?刚上三天班就被公司公司的工作不适合我,叫我先提升一下。 后面我也向公司那边讨要了一个说法,我只能说他们那边的说辞让我有些不服气。 现在之所以把这件事在掘金上记录一下,一是记录一下自己的成长轨迹,二也是想问问大家的看法  经过 我今年大三...
继续阅读 »

前言


还有谁?刚上三天班就被公司公司的工作不适合我,叫我先提升一下。


后面我也向公司那边讨要了一个说法,我只能说他们那边的说辞让我有些不服气。


现在之所以把这件事在掘金上记录一下,一是记录一下自己的成长轨迹,二也是想问问大家的看法 



经过


我今年大三,在江西的一所普通大学。


今年三月份开始学的前端,然后到了八月底,自己手上也有了两个玩具项目,就开始背一背八股文,也尝试去体验一下面试的滋味。


一直陆陆续续的突然发现了这家公司A,因为听说A可以接受大三远程实习,所以我就很努力的开始准备这家的面试。


到了九月底,我记得我那个时候还在上课,突然那边打电话给我,问我现在是否能面试?因为这次比较突兀,所以那边也没问什么问题,就问了一些很基础的react hooks以及深浅拷贝。然后就说我的面试通过了,但正式上班之前他要布置一个任务给我,叫我去模仿一下一款小程序,只要实现里面一些页面和功能即可。他说这些天就相当于我的试用期,看我配不配他们给我的100元一天的薪水。


大概到了10月13号,他们说我仿写的项目达到了他们的预期,可以把我的一些身份信息交给他了,然后17号正式入职。讲真的那个时候我还是很开心的,毕竟要开始自己第一份工作了,虽然收入不高,但足以让我不用再向家里要生活费了,以及后面寒假和明年暑假的时候也可以更好面一些大厂的实习。


10月17号,这一天我都没开始写代码,因为他那边的项目我运行不起来,而且他们那边的代码是用了函数式组件和类式组件混用,以及一些taro的知识。他们就干脆叫我自己重写一下他们的项目,用h5开发即可。


10月18号,我在这一天写了一白天的代码,自我感觉还可以,晚上到了git提交阶段,问题出现了,还是大问题。因为在git操作这一块我不太熟悉,自己之前也都是在gitee上和vscode绑定进行代码的提交,指令没怎么用过。然后用我自己用http复制仓库连接这一步它总报错,说拒绝连接。为此我在网上搜了好多种方法都没有找到解决办法,最后实在不得已问了他们。反正那一个晚上确实弄了好久也没弄好把我心态也给搞崩了,也把他们问的有点烦了。这一块是我自己的问题。


10月19号,那天一从床上蹦起来,我又开始了上传提交工作,果然睡了一觉效率高了很多,花了半个小时终于完成了昨天我一个晚上的工作。然后我又开始了项目的还原工作。一直到了晚上我才开始问了一个问题,问题都还没叙述完,他那边就来电话了,说我暂时不适合这份工作。。




沟通


他也打电话和我沟通了一下,就是他认为:


首先我样式写的有问题。关于这一点我在入职之前就和他说了我这一块不太好,但是我觉得我自己写的样式虽然不好,但是也绝对不会很差的那种,最多就是写的不够优雅。


其次就是他觉得我和他沟通有问题。比如这个项目,他叫我在他给的基础上完善一些功能设置一些接口,然后我和他说要不我先把他给的项目先还原了,再在这个基础上再进行添加。他说了OK。结果我做完后他又认为我还原页面还是不行,说这样写后面还要再重新写过,等于没写。。。


最后也是让我特别不解的地方,他说我要转变心态,不能以学习的心态来做这份工作。这我能理解,公司招人当然是要人来干活的,但我每天只会问他一两个问题,而且我在问这些问题之前都至少自己思考过半个小时以上才来找他。对于这点我自己也感觉迷,因为我知道问多了问题肯定会很麻烦人家,但如果不问问题,全靠我自己解决,这样每天的产出又很少,而且他那边都没有给我布置过每日任务,那几天都是我准备干什么然后和他说一声。


虽然他最后也说了,他还要考虑考虑,下个礼拜一给我答复。但是我今天还是忍不住,自己也和他说了一些我的看法。




我全文大概是这么说的:



在你考虑留不留我之前我也想说几句话,我觉得成年人处理事情应该要严谨一点。拿你对我的要求举例吧,首先至于我的能力,没错,我的能力肯定是有问题的,在面试和入职之前我就把我的问题和你说了,你也说这是小事,后面可以学。然后我是在你这里通过了面试的,面试通过起码能说明你对我能力的一种认可吧。面试通过后你叫我去仿小程序,说是试用期的意思,好,我也认真完成了,你也说我可以交作业了。这难道不意味着我试用期也通过了吗?然后我们开始谈协议,叫我签三个月还是六个月,叫我交材料。我现在还没进入社会,我也不知道你这边的协议有没有法律效力,但我还是很信任你,所以没和你谈协议这边的严谨性和合法性。结果我等来的就是工作三天被你说不合适,我还是个学生,没有进入社会也不知道这样算不算毁约,因为我不懂,而且对我来说这也不是个重点,重要的是一个公司对他们自己承诺的一个态度。其次,就是你说我要换一种心态,不能以学习的心态来做这份工作,我能理解,公司招人当然是要人来干活的,你平常的工作很忙我知道,总这样问你问题肯定会让你很烦,但我每天真的就是只会问你一两个问题,而且我在问这些问题之前都至少自己思考过半个小时以上才来找你的。对于这一点我也感觉很委屈,我知道问多了问题会麻烦你,但如果不问问题,全靠我自己解决,这样每天的产出肯定又不达标,不等你来说我,我自己也过意不去。问又不是,不问又不是。害。。。最后我觉得按你的要求的话,你不应该去招一天100的实习生来帮你干这种活,应该招正式员工。



最后问他要三天薪水的时候还被他扣了两天,第一天说我没提交代码,他说他之前和我说过,但我看协议上根本就没有写,然后第三天那个又不算。。。
更离谱的是一天薪水还说要过几天给我。。。。


补一张协议




最后


自己肯定是也有问题的,自身的实力肯定有待提升,但他的说辞真的令我不满意。


远程实习我也有点怕了,难道每天问一两个问题都不行吗?


另外我在合同法律这一块应该也要认真严谨一点,当时是真的比较信任他,所以没怎么注意。


害,过去了的就让它过去吧,可惜少了一段实习经历,但我也多出来了很多时间去学一些新的东西,可以好好准备一下寒假实习。


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

你可能并不需要useEffect

相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。 难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。 在这篇文章中,我将展示怎样使用其他方法来代替useEff...
继续阅读 »

相信大家在写react时都有这样的经历:在项目中使用了大量的useEffect,以至于让我们的代码变得混乱和难以维护。


难道说useEffect这个hook不好吗?并不是这样的,只是我们一直在滥用而已。


在这篇文章中,我将展示怎样使用其他方法来代替useEffect。


什么是useEffect


useEffect允许我们在函数组件中执行副作用。它可以模拟 componentDidMount、componentDidUpdate 和componentWillUnmount。我们可以用它来做很多事情。但是它也是一个非常危险的钩子,可能会导致很多bug。


为什么useEffect是容易出现bug的


来看一个定时器的例子:

import React, { useEffect } from 'react'

const Counter = () => {
const [count, setCount] = useState(0)

useEffect(() => {
const timer = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
})

return <div>{count}</div>
}

这是一个非常常见的例子,但是它是非常不好。因为如果组件由于某种原因重新渲染,就会重新设置定时器。该定时器将每秒调用两次,很容易导致内存泄漏。


怎样修复它?


useRef

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

const Counter = () => {
const [count, setCount] = useState(0)
const timerRef = useRef()

useEffect(() => {
timerRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(timerRef.current)
}, [])

return <div>{count}</div>
}

它不会在每次组件重新渲染时设置定时器。但是我们在项目中并不是这么简单的代码。而是各种状态,做各种事情。


你以为你写的useEffect

useEffect(() => {
doSomething()

return () => cleanup()
}, [whenThisChanges])

实际上是这样的

useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// 遗忘清理函数。
}, [foo, bar, baz, quo, ...])

写了一堆的逻辑,这种代码非常混乱难以维护。


useEffect 到底是用来干啥的


useEffect是一种将React与一些外部系统(网络、订阅、DOM)同步的方法。如果你没有任何外部系统,只是试图用useEffect管理数据流,你就会遇到问题。



有时我们并不需要useEffect


1.我们不需要useEffect转化数据

const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)

useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])

// ...
}

上面代码使用useEffect来进行数据的转化,效率很低。其实并不需要使用useEffect。当某些值可以从现有的props或state中计算出来时,不要把它放在状态中,在渲染期间计算它。

const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)

const totalNum = items.reduce((total, item) => total + item.price, 0)

// ...
}

如果计算逻辑比较复杂,可以使用useMemo:

const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])

// ...
}

2.使用useSyncExternalStore代替useEffect


useSyncExternalStore


常见方式:

const Store = () => {
const [isConnected, setIsConnected] = useState(true)

useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})

return () => {
sub.unsubscribe()
}
}, [])

// ...
}

更好的方式:

const Store = () => {
const isConnected = useSyncExternalStore(
storeApi.subscribe,
() => storeApi.getStatus() === 'connected',
true
)

// ...
}

3.没必要使用useEffect与父组件通信

const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])

return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Toggle quick view
</button>
</div>
)
}

更好的方式,可以使用事件处理函数代替:

const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)

const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)

if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}

return (
<div>
<button
onClick={}
>
Toggle quick view
</button>
</div>
)
}

4.没必要使用useEffect初始化应用程序

const Store = () => {
useEffect(() => {
storeApi.authenticate()
}, [])

// ...
}

更好的方式:


方式一:

const Store = () => {
const didAuthenticateRef = useRef()

useEffect(() => {
if (didAuthenticateRef.current) return

storeApi.authenticate()

didAuthenticateRef.current = true
}, [])

// ...
}

方式二:

let didAuthenticate = false

const Store = () => {
useEffect(() => {
if (didAuthenticate) return

storeApi.authenticate()

didAuthenticate = true
}, [])

// ...
}

方式三:

if (typeof window !== 'undefined') {
storeApi.authenticate()
}

const Store = () => {
// ...
}

5.没必要在useEffect请求数据


常见写法

const Store = () => {
const [items, setItems] = useState([])

useEffect(() => {
let isCanceled = false

getItems().then((data) => {
if (isCanceled) return

setItems(data)
})

return () => {
isCanceled = true
}
})

// ...
}

更好的方式:


没有必要使用useEffect,可以使用swr:

import useSWR from 'swr'

export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)

if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>

return <div>hello {data}!</div>
}

使用react-query:

import { getItems } from './storeApi'
import { useQuery, useQueryClient } from 'react-query'

const Store = () => {
const queryClient = useQueryClient()

return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
See items
</button>
)
}

const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)

// ...
}

没有正式发布的react的 use函数

function Note({ id }) {
const note = use(fetchNote(id))

return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}

reference


http://www.youtube.com/watch?v=bGz…


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

将项目依赖从 CocoaPods 迁移到 SPM

iOS
昨天的文章讲了如何删除项目中的 CocoaPods 依赖,文章中也有提到未来的趋势一定是从 CocoaPods 到 Swift Package Manager(SPM),今天就来讲讲如何添加 SPM 依赖。 SPM 是苹果在2018年推出的供 Swift 开发...
继续阅读 »


昨天的文章讲了如何删除项目中的 CocoaPods 依赖,文章中也有提到未来的趋势一定是从 CocoaPods 到 Swift Package Manager(SPM),今天就来讲讲如何添加 SPM 依赖。


SPM 是苹果在2018年推出的供 Swift 开发者进行包管理的工具,从 Xcode 11 开始支持。


首先打开 Xcode,点击项目根目录,选择 PROJECT,然后选择第三个 Tab,Package Dependencies,最后点击下边的加号按钮。



之后会出现 Package 的选择面板:



然后在右上角的输入框中输入你要依赖的项目地址,如果不知道项目地址可以到依赖包的官方页面查看,比如我们要添加 Alamofire,就可以到其 Github 页面 github.com/Alamofire/A…,文档中有 Swift Package Manager 的安装方法:




拷贝这个地址复制到前边说的输入框内,Xcode 会自动帮我们找到这个库,在右侧可以选择你需要依赖的版本以及对应的 Target:




最后点击右下角的 Add Package 按钮,随后 Xcode 会下载这个仓库,并弹出面板让我们选择要添加到哪个 Target,最后再次点击 Add Package 即可



添加完成后,我们就可以在 Xcode 项目中看到这个依赖被成功添加进来了。



之后你就可以开始愉快的使用它们了:

import UIKit
import Alamofire

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
AF.request("https://apple.com").response { res in
debugPrint(res)
}
}
}


最后我还下载了一些 swift 开发中主流的一些库,安装都很快,用起来可以说非常方便了。



除了在 GitHub 上找 swift 包之外,Swift Package Index(SPI) 也是一个不错的选择,SPI 是一个开源的 swift 包集合地,这里包含了大量的 swift 开源库,并且在前不久,苹果官方赞助了 SPI,以确保它能正常的发展下去,在不久的将来,Swift 开源库可能不支持 CocoaPods,但一定会支持 Swift Package Manager。


参考资料


[1]


Swift Package Index: swiftpackageindex.com/


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

iOS 判断系统版本

iOS
方案一double systemVersion = [UIDevice currentDevice].systemVersion.boolValue; if (systemVersion >= 7.0) { // >= iOS 7.0 }...
继续阅读 »

方案一

double systemVersion = [UIDevice currentDevice].systemVersion.boolValue;

if (systemVersion >= 7.0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

if (systemVersion >= 10.0) {
// >= iOS 10.0
} else {
// < iOS 10.0
}

如果只是大致判断是哪个系统版本,上面的方法是可行的,如果具体到某个版本,如 10.0.1,那就会有偏差。我们知道 systemVersion 依旧是10.0。


方案二

NSString *systemVersion = [UIDevice currentDevice].systemVersion;
NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1" options:NSNumericSearch];

if (comparisonResult == NSOrderedAscending) {
// < iOS 10.0.1
} else if (comparisonResult == NSOrderedSame) {
// = iOS 10.0.1
} else if (comparisonResult == NSOrderedDescending) {
// > iOS 10.0.1
}

// 或者

if (comparisonResult != NSOrderedAscending) {
// >= iOS 10.0.1
} else {
// < iOS 10.0.1
}

有篇博客提到这种方法不靠谱。比如系统版本是 10.1.1,而我们提供的版本是 8.2,会返回NSOrderedAscending,即认为 10.1.1 < 8.2 。


其实,用这样的比较方式 NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1"],的确会出现这种情况,因为默认是每个字符逐个比较,即 1(0.1.1) < 8(.2),结果可想而知。但我是用 NSNumericSearch 方式比较的,即数值的比较,不是字符比较,也不需要转化成NSValue(NSNumber) 再去比较。


方案三

if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

// 或者

if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

这些宏定义是 Apple 预先定义好的,如下:

#if TARGET_OS_IPHONE
...
#define NSFoundationVersionNumber_iOS_9_4 1280.25
#define NSFoundationVersionNumber_iOS_9_x_Max 1299
#endif


细心的童靴可能已经发现问题了。Apple 没有提供 iOS 10 以后的宏?,我们要判断iOS10.0以后的版本该怎么做呢?
有篇博客中提到,iOS10.0以后版本号提供了,并且逐次降低了,并提供了依据。

#if TARGET_OS_MAC
#define NSFoundationVersionNumber10_1_1 425.00
#define NSFoundationVersionNumber10_1_2 425.00
#define NSFoundationVersionNumber10_1_3 425.00
#define NSFoundationVersionNumber10_1_4 425.00
...
#endif


我想这位童鞋可能没仔细看, 这两组宏是分别针对iPhone和macOS的,不能混为一谈的。


所以也只能像下面的方式来大致判断iOS 10.0, 但之前的iOS版本是可以准确判断的。

if (NSFoundationVersionNumber > floor(NSFoundationVersionNumber_iOS_9_x_Max)) {
// > iOS 10.0
} else {
// <= iOS 10.0
}

方案四


在iOS8.0中,Apple也提供了NSProcessInfo 这个类来检测版本问题。

@property (readonly) NSOperatingSystemVersion operatingSystemVersion NS_AVAILABLE(10_10, 8_0);
- (BOOL) isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version NS_AVAILABLE(10_10, 8_0);

所以这样检测:

if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 3, .patchVersion = 0}]) {
// >= iOS 8.3
} else {
// < iOS 8.3
}

用来判断iOS 10.0以上的各个版本也是没有问题的,唯一的缺点就是不能准确版本是哪个版本,当然这种情况很少。如果是这种情况,可以通过字符串的比较判断。


方案五


通过判断某种特定的类有没有被定义,或者类能不能响应哪个特定版本才有的方法。
比如,UIAlertController 是在iOS 8.0才被引进来的一个类,我们这个依据来判断版本

if (NSClassFromString(@"UIAlertController")) {
// >= iOS 8.0
} else {
// < iOS 8.0
}

说到这里,就顺便提一下在编译期间如何进行版本控制,依然用UIAlertController 来说明。

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIAlertController : UIViewController

NS_CLASS_AVAILABLE_IOS(8_0) 这个宏说明,UIAlertController 是在iOS8.0才被引进来的API,那如果我们在iOS7.0上使用,应用程序就会挂掉,那么如何在iOS8.0及以后的版本使用UIAlertController ,而在iOS8.0以前的版本中仍然使用UIAlertView 呢?


这里我们会介绍一下在#import <AvailabilityInternal.h> 中的两个宏定义:


*__IPHONE_OS_VERSION_MIN_REQUIRED


*__IPHONE_OS_VERSION_MAX_ALLOWED


从字面意思就可以直到,__IPHONE_OS_VERSION_MIN_REQUIRED 表示iPhone支持最低的版本系统,__IPHONE_OS_VERSION_MAX_ALLOWED 表示iPhone允许最高的系统版本。


__IPHONE_OS_VERSION_MAX_ALLOWED 的取值来自iOS SDK的版本,比如我现在使用的是Xcode Version 8.2.1(8C1002),SDK版本是iOS 10.2,怎么看Xcode里SDK的iOS版本呢?



进入PROJECT,选择Build Setting,在Architectures中的Base SDK中可以查看当前的iOS SDK版本。



打印这个宏,可以看到它一直输出100200。


__IPHONE_OS_VERSION_MIN_REQUIRED 的取值来自项目TARGETS的Deployment Target,即APP愿意支持的最低版本。如果我们修改它为8.2,打印这个宏,会发现输出80200,默认为10.2。


通常,__IPHONE_OS_VERSION_MAX_ALLOWED 可以代表当前的SDK的版本,用来判断当前版本是否开始支持或具有某些功能。而__IPHONE_OS_VERSION_MIN_REQUIRED 则是当前SDK支持的最低版本,用来判断当前版本是否仍然支持或具有某些功能。


回到UIAlertController 使用的问题,我们就可以使用这些宏,添加版本检测判断,从而使我们的代码更健壮。

 - (void)showAlertView {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Title" message:@"message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
#else
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_0) {
UIAlertController *alertViewController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *otherAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];

[alertViewController addAction:cancelAction];
[alertViewController addAction:otherAction];

[self presentViewController:alertViewController animated:YES completion:NULL];
}
#endif
}

方案六


iOS 11.0 以后,Apple加入了新的API,以后我们就可以像在Swift中的那样,很方便的判断系统版本了。

if (@available(iOS 11.0, *)) {
// iOS 11.0 及以后的版本
} else {
// iOS 11.0 之前
}

参考链接


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

我的天!多个知名组件库都出现了类似的bug!

前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design,字节系:arco design...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:




这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:




如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性


  • will-change: transform;
  • will-change: filter;
  • will-change: perspective;
  • transform 不为none
  • perspective不为none
  • 非safari浏览器,filter属性不为none
  • 非safari浏览器,backdrop-filter属性不为none
  • 等等

都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):

  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

  • transform 或 perspective 的值不是 none
  • will-change 的值是 transform 或 perspective
  • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。
  • contain 的值是 paint(例如:contain: paint;
  • backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);

评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案


  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值
  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了

具体代码如下:


  • offsetParent固定元素的定位上下文,也就是相对定位的父元素
  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定
affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文

export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:

import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


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

紧急需求‼️实现iOS启动图动态置灰

iOS
前言 相信这几天各大互联网应用首页置灰已经接踵而至,事情缘由我就不太赘述。毫无疑问,我司从30号当晚就收到紧急需求,我们要求1号必须紧急发版,除了常规的首页支持配置的动态置灰外,我们还要求另外一个需求就是,启动图也需要支持动态配置灰功能,经过几个同事的努力,于...
继续阅读 »

前言


相信这几天各大互联网应用首页置灰已经接踵而至,事情缘由我就不太赘述。毫无疑问,我司从30号当晚就收到紧急需求,我们要求1号必须紧急发版,除了常规的首页支持配置的动态置灰外,我们还要求另外一个需求就是,启动图也需要支持动态配置灰功能,经过几个同事的努力,于1号当晚顺利的发版了,第二天一早便成功上线,在此记录一下实现iOS启动图动态置灰的方案心得。


方案过程


实话说,当我接到此需求时,我负责的是实现iOS启动图动态置灰,当时我不太确认是否能实现,我能想到的是马上搜百度、谷歌、掘金等看是否有现成的轮子,答案肯定是有的,分别是



此方案非常轻量级,只有BBADynamicLaunchImage一个类,功能也只有一个,即查找系统缓存的启动图路径,使用我们提供的UIImage替换掉。其他版本控制本非必要需求我们自己代码控制即可。最终我也是直接采用了这个方案,其他控制由我代码自己编写核心方法如下。PS:(虽然提供iOS13之前的启动图路径查找,但是经过我实测一台iOS12的设备是不生效的,只有iOS13意思机型生效)


/// 系统启动图缓存路径

+ (NSString *)launchImageCacheDirectory {

NSString *bundleID = [NSBundle mainBundle].infoDictionary[@"CFBundleIdentifier"];

NSFileManager *fm = [NSFileManager defaultManager];

// iOS13之前

NSString *cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];

NSString *snapshotsPath = [[cachesDirectory stringByAppendingPathComponent:@"Snapshots"] stringByAppendingPathComponent:bundleID];

if ([fm fileExistsAtPath:snapshotsPath]) {

return snapshotsPath;

}

// iOS13

NSString *libraryDirectory = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];

snapshotsPath = [NSString stringWithFormat:@"%@/SplashBoard/Snapshots/%@ - {DEFAULT GROUP}", libraryDirectory, bundleID];

if ([fm fileExistsAtPath:snapshotsPath]) {

return snapshotsPath;

}

return nil;

}



稍微吐槽下这个库,此库也是我一开始使用的。它也是基于BBADynamicLaunchImage做了一些拓展。比如版本控制,但是它内置的版本控制有漏洞,它只支持CFBundleShortVersionString,也就是我们俗称的大版本,如果我build号改了版本号不变岂不是有问题?(这也是我打包后不生效调试了好久才发现的问题)而且要支持动态置灰,不发版恢复原图就更加有问题。最后也是弃用了,当然这个库支持暗黑模式下的启动图,但是我本身app就是不支持的这个功能就聊胜于无了,最终该用了上边的方案,动态控制由我自己处理。


启动图如何置灰


要实现启动图和原图一模一样只是变成灰白,这里就稍微要花一点点心思了。众所周知我们现在iOS启动图都是直接用LaunchScreen这个Storyborad生成的,那我们是否能加载这个LaunchScreen,然后截取UIView的图片,之后再通过bitmap转换成一张灰白图?答案是显而易见的,代码如下。


首先我们要给LaunchScreen定义一个id,因为默认没有人去加载它,它也没有id。



代码如下:


生成启动图原图或灰白图方法,注意此方法要在主线程跑。


+ (UIImage *)createLaunchScreenImage:(BOOL)isNeedGray {

UIStoryboard *sb = [UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil];

UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"LaunchScreen"];

[vc loadViewIfNeeded];

vc.view.frame = UIScreen.mainScreen.bounds;

UIImage *image = [vc.view snapshotImage];

if (isNeedGray) {

image = [image createGrayImage];

}

return image;

}



UIView截图

func snapshotImage() -> UIImage? {

UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0);

self.layer.render(in: UIGraphicsGetCurrentContext()!)

let image = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()

return image

}


生成灰白图方法,由于启动图必须size匹配,所以scale那些要处理好。


-(UIImage*)createGrayImage {

int width = self.size.width * self.scale;

int height = self.size.height * self.scale;

CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();

CGContextRef context =CGBitmapContextCreate(nil,

width,

height,

8,// bits per component

0,

colorSpace,

kCGBitmapByteOrderDefault);

CGColorSpaceRelease(colorSpace);

if(context ==NULL) {

return nil;

}

CGContextDrawImage(context,

CGRectMake(0,0, width, height), self.CGImage);

UIImage*grayImage = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context) scale:self.scale orientation:self.imageOrientation];

CGContextRelease(context);

return grayImage;

}


动态替换


我们只需要请求后台配置,需要灰白就提供灰白图,当配置失效,需要还原时候,根据上面方法,直接渲染一个LaunchScreen原图即可,当然其中还要做好持久化控制,不要处理多次替换,替换生效后不再处理。


末尾


以上就是我实现此次iOS启动图动态置灰的全过程,由于过程的艰辛,加之我自己是一个Swifter。估计不久将来,我也会基于Swift写一个稍微友好点的库,在此立个Flag。


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

人生不必要精彩,但得有故事

今年已然过半了,乘着中午午休时间,写点感想吧,或许可作为今年的年中总结。 目前现状 话题来源于中午吃饭和同事的闲聊,问及平时都有些什么爱好。我们双方细想了一阵后都噗嗤一笑,发现人到了中年好像很难再有真正的兴趣爱好。 平时除了工作,下班后回家陪陪孩子,再加上处理...
继续阅读 »

今年已然过半了,乘着中午午休时间,写点感想吧,或许可作为今年的年中总结。


目前现状


话题来源于中午吃饭和同事的闲聊,问及平时都有些什么爱好。我们双方细想了一阵后都噗嗤一笑,发现人到了中年好像很难再有真正的兴趣爱好。
平时除了工作,下班后回家陪陪孩子,再加上处理一些家庭琐事。几乎抽不出什么时间做一些自己感兴趣的事了。



究其原因,我觉得有两点:



  1. 精力不足,时间不够用

  2. 随着时间的消磨,渐渐对曾经感兴趣的事没有了热情


如果是第一点,我觉得还好,时间不够用,还可以通过规划,提高效率来解决。但如果一旦是对生活和工作没有了激情,那就很危险了。需要我们来做个自我反思了。


都说上班工作的人,每天的生活按部就班,年复一年,眼里渐渐没有了光。


遥想当年毕业时我们信心满满,踌躇满志。可如今我们却被曾今挚爱的工作折磨的毫无斗志,是我们选择了向生活妥协。


生活和理想之间出现了一道鸿沟。我们需要重新拾起那份挚爱,找寻失去的那道光。


时间从来不以人的意志为转移,不管你怎么对待你的生活,它都是稍纵即逝。为何不让自己的生活过的更丰富些!


人生不必要精彩,但得有故事,这就是今天要聊的主题。


作为程序员的自己,还是有点折腾的劲在的。看了很多互联网技术从业者的职业打怪升级之路。好像很多都与写作有关,再就是自由开发者那一类,他们有的后来转到了自媒体,
有的做出了很不错的 side project,最终被收购实现财务自由。人生有很多的可能性,不要被自己的职业工作所裹挟。


业余工作


今年除了工作在有条不紊的进行,我的业余时间一部分被孩子占了,周末会带着她出去玩玩,下班后回家也会陪着她玩会。睡觉前跟她互动直到睡着。这么一看,留给自己的时间
也不多了。虽然时间比较紧凑,但也还是产出了点成果。


主要内容有:



  1. 前端开发笔记的博客每月都有持续地更新,期间会同步一些好的文章到各个知识平台(掘金、简书、CSDN、开发者头条);

  2. 从五月份开始,重构了“太空编程” 站点,陆陆续续在完善和新增了一部分内容:

    • 十几篇博客文章

    • 四五个独立页面,有工具和代码示例的介绍

    • 知识库作为前端知识体系的梳理和总结



  3. 学习 React 框架开发,作为实战产出了几个应用小工具:

    • VISITOR BADGE

    • 分割线工具

    • 代码图片生成工具



  4. 公众号偶有推送认为写的比较好的文章


收获成果


另外出乎意料的是,“前端开发笔记” 博客关于 磁力猫 的关键字搜索每天都有可观的流量进来,
后来竟然有广告主主动找来,买下了广告链接。这样就有了一份意外的收入。


百度统计


从三月份到现在持续在续费,没想到会这么久。



网站上挂了 Google Ads 平台广告,目前收入情况:



公众号粉丝量也已经达到了 1269,虽然目前每月只有少得可怜的流量主广告收入。


掘金上有两篇高赞收藏量很高的文章,截止目前:



  • 如何让自己的前端知识更全面 赞 842收藏 2905

  • 前端 UI 组件库有哪些选择 赞 246收藏 685


掘金账号等级到了 LV.5,粉丝突破 200,网站很大一部分流量来自掘金,所以让我坚信要用很长一段时间好好把账号养起来。


总结


今年会围绕以上四点内容持续发力,努力产出优质的分享内容。把自己的前端知识体系打牢,深入 React 开发实践是目前的主要任务。现在在写作方面是能写了,但是质量不高,要想写出通俗易懂的
技术文章还有很长的一段路要走。


生命不息,折腾不止!


作者:编程范儿
来源:juejin.cn/post/7261533443895935033
收起阅读 »

当一个程序员从产品角度思考技术

近期做需求中有一些感想,在这里记录下。 1. 产品究竟是谁的 人人都是产品经理,并不是一句口号,每个人都是用户,即便你是研发、设计、测试,都有无数的时间,和世界上最优秀的产品打交道。最优秀的产品包括你的电脑、手机,和那些最流行的APP。 所以,如果某个产品经理...
继续阅读 »

近期做需求中有一些感想,在这里记录下。


1. 产品究竟是谁的


人人都是产品经理,并不是一句口号,每个人都是用户,即便你是研发、设计、测试,都有无数的时间,和世界上最优秀的产品打交道。最优秀的产品包括你的电脑、手机,和那些最流行的APP。


所以,如果某个产品经理一言堂,那这个产品大概率不会太成功。除非他真的厉害,并且有数据支撑,比如乔布斯这种。产品不是政治,产品是可以讨论的。


如果一些交互设计难以理解,研发都用着费劲,怎么在普通人中推广呢?


另一个问题,研发需要了解并思考产品的逻辑吗?


答案肯定是需要的,产品是有逻辑的,研发可以根据产品的逻辑,判断、预测未来的变化,抽出稳定性更强的代码。


一个工作3年和工作10年的研发的一个区别就在于,对于产品逻辑的理解,大概工作更久的人更容易预测产品未来的变化,会在代码编写、结构设计时就拆分出不变与变化,从而让代码更易维护。但是,如果每次新来一个需求,都从0开始,从不思考,就另当别论了。


2. 定制与通用


通用和定制往往是对立的,一个需求如果是定制的,意味着不够通用。


前一阵做了一个国际化的需求,国际化其实就代表通用,本地化代表定制。


但是如果同一类型定制的多了,就可以变为通用需求。本质上一个string/number/boolean类型总是可以扩展成array的。


比如H5的自动化测试报告处理,和小程序的处理,是可以通用化的。


这给我们一个启示是,要多沉淀东西,做过的那么多需求,比如表单系统、权限系统,这些工作即使一两年内不会重复,但是时间拉长来,总会在职业生涯的某个阶段重新做一次。


还有,我们加班那么多,是不是产品设计、需求提出、技术实现的时候,通用性考虑的就不足呢,所以今天在A上拧一个螺丝,明天在B上拧一个螺丝。


可怕的是,这两个螺丝都是一样的残次品。


通用性在不同的层次上有不同的表现,比如总监可以考虑业务的通用,产品经理考虑产品的通用,开发则考虑代码和系统层面的。


组件化的基础也是通用性,如果一个组件可以被复用,说明它是通用的。


3. 复用性


应当努力追求高复用,不论是组件,还是工具,为什么呢?



  1. 从零实现成本高

  2. 复制粘贴然后改一点,看似成本也不高,后续维护、迭代更新则成本高


实践证明,组件、工具的迭代更新的频率,一般是高于认知的。即便你当下觉得它很稳定了、很完美了,也很有可能在未来某个时候优化,因为人的认识总是有局限性的


组件高复用,可以统一升级、统一优化、统一迭代。不同维度的组织都应该追求高复用,包括个人、项目、团队。


4. 字段名


发现一个现象,大家在字段命名的时候比较洒脱。



  • 格式比较多样,比如角色Id:roleIdrole_idroleid,子活动Id:childidchild_idchildId

  • 名称也多样,比如手机号:phonemobile,头像:headheader


大家好像习惯了这种乱七八糟的命名,倘若统一了,反而觉得难受。


在其他地方没得到的自由,终于在命名时得到了。


这种字段命名问题和上面的通用化有些关系,如果不能统一,谈何通用呢,通用化的基础一定是标准化。


为什么大家会在命名时候,这么随意呢?大概率是因为他根本没考虑过下游开发者的感受,比如前端。


下游开发者做起来是很痛苦的,他知道这个协议会包含子活动Id,但是他必须去协议页面查看,才能知道究竟是哪种命名,是childid、还是childId、还是child_id?并且,他在向他的下游或下级页面、组件传递时,需要考虑对方需要的是什么,必须做一层转换。如果他不知道转换或者不想转换,让下游直接修改字段名,那整个项目就更“精彩”了。


意识也是一种能力。能够预见到一些可能带来的问题,并尽量规避掉,也是一种好习惯。


5. 代码是资产还是负债


总看到有人说,代码是负债,个人不敢苟同,倘若代码都是负债,你还在这敲锤子啊,岂不是越欠越多,回家睡大觉不是更好?


大部分人之所以认为代码是负债,是因为维护起来费劲,一看代码就头疼,每次改代码都像是上刀山,这能不是负债吗。


或者改别人写的代码,小心翼翼、胆战心惊、如履薄冰,生怕解决一个问题,带来其他问题。


所以,个人认为,质量高的代码是资产,写的像X一样的是负债。


“大处着眼,小处着手”,架构、系统相关可以视为“大处”,代码规范、代码质量可以视为“小处”。


最近看到公共基础库很多很多的魔法字符串,开发没有意识去抽离,维护这样的代码就比较头疼了。


好的代码一般是模块化,抽象化,通用化,配置化。失控的代码,没有模块化,没有整理,没有规律,没法维护,没法复用。没有解耦,维护起来相当麻烦。


甚至有人说,“单元测试有什么用?“


6. 兜底


有一个需求,A类型的奖品不需要展示,B类型的奖品可以展示,后台让前端“兜底”,过滤下这个类型的奖品。


想到这个只是产品的上层逻辑,是极容易变化的,今天屏蔽A、明天屏蔽B,后天可能又要放开A、B。


兜底应该是对边界的兼容,比如:



  • 多重if else中最后的else

  • 对空值给默认值

  • 对解析失败的捕获

  • 对不同机型、不同环境的兼容性


像这种产品上的逻辑怎么会是兜底呢?这种过滤其实是数据层面的处理,越在上游处理越简单,因为用数据的地方总是无法控制的。


这其实反映出另一个问题,前后台责任不明确,这次你帮我多处理一下,下次我帮你多处理一下。本来后台该做的事,放到前端来做,下一次,本来前端该兼容的,让后台来做。


前端应该始终兼容空值边界,比如a.b,当a不存在时候的取值异常。组件、页面总是可能复用的,如果下个prop或者cgi没传,就要付出额外的时间成本处理错误。


前端兼容性大多是为了系统鲁棒性。


此外,处理数据的逻辑应该放在一起,如果这里增加一点字段、那里改变一下结构,又没有类型提示的话,后期维护起来很难受。


前端当变量为undefinednull时,会报错的语法或API:


Object.keys(null)
Object.keys(undefined)
// 会报错,Uncaught TypeError: Cannot convert undefined or null to object

a.toString(); // a 为 undefined 或 null
// Uncaught TypeError: Cannot read properties of null (reading 'toString')

7. 兼容


上面提到了兼容,其实兼容有两种:



向前兼容(forward compatibility) = 向上兼容(upward compatibility),也就是向未来兼容,即现在设计的软件要考虑未来还能不能用。
向后兼容(backward compatibility) = 向下兼容(downward compatibility),也就是向过去兼容,即现在设计的软件要考虑旧版本的数据还能不能用。



一般说的兼容指的是向后兼容。在框架、基础库升级的过程中,如何实现向后兼容、平稳过渡呢?


通常是设置一个过渡版本,比如v1版本的旧API,在v2版本时候同时提供新老两种写法,并标明旧APi即将废弃,然后在v3版本正式废弃掉旧API。


如果不提供过渡版本,一般会导致开发者不敢升级,比如vue2vue3就没有做到平滑过度,导致现在很多项目都是vue2


8. 变化


代码维护、迭代过程中最重要的事情之一就是控制变化,应该始终将变化的部分做到可控。


其实现代社会就很符合这个规范,电网、能源、航空、铁路等,这些核心产业都被国家握在手里,其他一些小打小闹的产业,比如餐饮、互联网等,随私企去折腾。


对于大型项目,核心模块,比如基础UI组件库、网络框架、核心逻辑,都应该以一种稳定的形式存在,比如npm包、基础库,不能今天改、明天又改。


为什么函数粒度要细呢,也就是常说的一个函数只完成一个功能?其实本质也是控制了变化,如果一个函数同时完成多个功能,那么改动的可能性就更高,就更容易出错。


稳定性是我们一直追求的目标,一般来说,我们更喜欢发挥稳定的球员,而不是“神经刀”。


代码也是一样,控制变化,其实就是保持稳定性。


为什么要追求稳定呢?本质上是变化的成本太高了,越复杂、越底层的组件、工具,改动风险越高,因为复用的地方多,很可能牵一发而动全身。即使做到了高内聚、低耦合,如果改动不是向下兼容的,上层就要一起更新,会增加时间成本、出错概率。




稳定并不意味着一成不变、一定没有变化,比如一个组件库,内部的优化可以一直做,只是对外的API、展现的形式需要稳定,也就是与之前保持一致。


9. 写文章


写文章会耽误工作吗?就个人经验来说,写文章不但不会影响工作,反而会提升效率。


因为写文章一定是因为有自己的思考才写,不论是解决了问题、还是总结了方法,都是有或多或少的思考,大脑一定是活跃的


没写文章的时候,看似工作时间投入更多,其实脑子已经不转了,没有自己的理解,没有总结思考,工作效率其实非常低。


这就好像,那些成绩好的同学其他方面也有特长,比如体育、文艺,反而是那些成绩差的才干啥啥不行。


为什么会这样呢?因为思考这个东西是相通的,好多东西底层是一样的,会了一样就可以触类旁通、举一反三。


10. 低代码


最近人工智能生成代码的方式很火,更新了人们对“低代码”的认知。低代码、零代码的本质是为了提升开发效率,提高生产力,只要能达到此目的的都是低代码。


因此,广义的低代码有下面几种:



  • 从手上已有的其他项目中复制粘贴,比如之前有了一个项目表格,现在要做一个操作记录表格,直接复制粘贴然后改改即可。

  • 从搜索引擎中搜索,然后复制搜到的内容

  • 从一些可视化界面,拖拽组件而成,即目前人们常说的低代码平台

  • 人工智能生成


哪种会成为未来的主流呢?衡量标准有下面几个:



  1. 实现成本低,狭义的低代码平台需要搭建,而且越复杂的项目搭建成本越高

  2. 实现效果好,开发者改动少,并且接近需求


从上面的标准来看,个人目前看好AI。


11. Mixin


有许多人很排斥使用Mixin,弃之如敝屣,其实只是使用姿势不对而已。



  1. 一个Mixin文件中的内容应该高内聚,需符合单一职责原则,围绕一个中心,不能做两件事。

  2. 尽量减少使用全局Mixin,多用单文件引入。


只要做到上面两点,Mixin就可以化为高复用的利器。


12. 小而美


“小”的优点:



  1. 易于理解和学习。如果你想要写出全世界都是用的程序,那这一点很重要,无论是大牛还是小白,都能轻松是用,才能推广开来。

  2. 易于维护。即便是自己写的代码,过半年自己都忘记当时写的是什么了,要考虑这一点。

  3. 消耗更少的资源。“小”到制作一件事,用多少就消耗多少,不做一点额外的开销和浪费。

  4. 更易于和其他工具结合。即可扩展性更好,符合开放封闭原则。


让每个程序只做好一件事(单一职责原则),这个和准则1(小即是美)表达的意思一致。


只做好一件事,说明足够小。越是大型的系统,这个原则越重要,否则越大就越乱。


书中列举了一个范例 —— ls命令。ls本来是很简单的一个命令,现在却搞的有 20 多个参数,而且正在逐步增加。这就使得ls慢慢变成一个很庞大的命令,但我们日常 90% 的场景都使用它最简单的功能。理想的做法是,ls还保持简洁的功能,另外开发新的命令来满足其他配置参数实现的功能。这就例如,cat可查看全部内容,想看头或者尾,分别使用headtail——这就分的清晰了。


某BG就是典型的小而美,灵活。


13. 沉淀


沉淀了多少组件、公共方法,这才是业务开发中重要的。


沉淀是分等级的。项目越大,项目分层越多,沉淀等级越多。底层的更纯粹,应当尽可能沉到底层。以最近维护的几个项目为例:


t-comm > uni-plugin-light > press-ui

组件库由于必须依赖框架,所以不能和JS/TS工具一样可以沉到最底层。


一句话总结就是,能沉到多底就沉到多底。


14. 研效


很多人都在提研效,PPT里、总结邮件里,但其实并没懂研效的本质,个人看来,研效关键点有两个:



  1. 是否有足够多自动化的工具,比如CI、配置中心、自动化测试、自动同步工具、任务调度平台

  2. 是否沉淀了足够多的组件,新需求来了后,可以调用现成的能力,需求做完了后,又沉淀了另一些能力


如何判断一个部门、团队、个人是否对研效敏感呢?只要看他对上面两个方向的重视程度就行了。


堆人力,轻研效的特点是:



  1. 对工具轻视、恐惧,喜欢最原始的方式,喜欢刀耕火种

  2. 对沉淀轻视、恐惧,做什么都是一次性的,从不总结


15. 简单



Keep it simple. Keep it stupid.



这句话固然是对的,个人理解更重要的是“化繁为简”。业务不可能一直简单,组件库、工具不可能没有复杂的部分,重要的是有化繁为简的能力。


什么是化繁为简的能力呢,应该包括抽象能力、理解能力、总结归纳、聚类、举一反三等。


16. 耦合


什么是耦合?


程序里的耦合指的是依赖关系,比如A模块中引入了B、C、D模块的内容,就产生了耦合。


如果A、B两个项目用脚本同步一些代码,算耦合吗?当然不算,根本没有依赖关系的产生,删掉A,B一样可以运行。


JS中函数是一等公民,函数中的参数传递是耦合的最佳方式。挂载在window或文件中的全局变量上,这种耦合方式是最差的,难维护的,难以追踪的。


17. 大组件


大组件并不意味着大文件,press-ui可以提供由多个小组件构成的大组件,但不能是大文件,因为这样不灵活,如果需要变更、扩展的话,更改的东西多,容易出错。


同时,大组件意味着events会很多,props很多或者很大。


大组件在有些场景下,是有好处的。比如这个组件很多地方要用,比如横竖版、管理端都要用,那么把这个组件封装下,一个mode就可以解决多端问题,到了业务层就不用写太多重复代码了。


18. 就近原则


组件的样式应该就近写在组件附近,而不能是在page层覆盖。为什么呢,因为一旦多个page都使用这个组件,那么覆盖关系就很难追踪。而如果只有一个page使用,那么就应该写在page的附近,不必提取到组件库中。


19. 新工具


前端框架更新很快,有人可能会说华而不实,花里胡哨,okr项目,没有vue2香。


这里简单分析下用新技术的必要性。




  • 从学习角度上,一个社区流行的新框架、新工具,能让诸多项目自发迁移、升级,不是公司因为各种利益强推的,一定是做对了什么,不管是原理、方法论还是思想,都值得去学习。




  • 从生产力角度上,新框架大规模取代旧工具,一定在开发效率、性能、可维护性等方面有提升,而且一般不止一个维度。作为上层应用开发者(API工程师),更应该利用好这些工具。




  • 先发优势,一般新框架稍微稳定一点时,做有关它的生态工具会比较容易得到广泛应用。比如vue3刚稳定时,做个vue3组件库,容易推广,如果现在做,基本很难推了,该趟过的坑都趟过了,也经过了生产环境的考验,凭什么让别人用你的呢?




当然了,你永远无法叫醒一个装睡的人。


20. 人


一个工具会被写成什么样子,取决于人。


举个例子,某库迭代了一次又一次,一直没有稳定,又开始改,个人觉得基本还是白搭。因为开发者还是那一批人,他们之前写什么样的代码,之后大概也会写成什么样。


没有方法论的升级,没有任何反思,不懂抽离,不懂封装,只是换个地方,套个壳而已。


21. t-comm



  • 尽量分类,不要什么都往utils目录下放,否则utils目录会爆炸,难以寻找

    • 如果贪图一时快,都放到utils中,无疑是给自己挖坑,以后还需要重构

    • index.ts中行数,等于ll src | grep '^d'| wc -l的值-1,排除types



  • 一定在导出的第一层,进行导出文件的指定,第二层、第三层等后面的导出要用*

    • 第一层不能用*,否则一些方法不想导出,也被导出了

    • 后面不能指定,因为前面指定过了,后面再指定就重复了,而且改很麻烦,容易遗漏




// base/function/index.ts
export {
parseFunction,
cached,
} from './function';


// base/index.ts
export * from './function';
export * from './list';
export * from './number';
export * from './object';
export * from './string';


// index.ts
export * from './base';


  • 按照文件的ASCII顺序,也就是文件/文件夹的默认顺序,来导出文件,这样容易对比,不容易遗漏


export * from './base';
export * from './canvas';
export * from './clipboard';
export * from './color';
export * from './cookie';
export * from './cron';

22. 理解


有时候我们能把事情做成什么样,取决于我们对它的理解。


你如果把微信当成抖音做,大概做不好。苹果就有一个口号,“Think Different”,这是它的格局,也是他们对自己产品的理解。


做组件库、基础库,甚至是普通需求也要有自己的理解。


作者:Novlan1
来源:juejin.cn/post/7277798325637070889
收起阅读 »

揭秘外卖平台的附近公里设计

背景 相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。 分析 我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的...
继续阅读 »

背景


相信大家都有点外卖的时候去按照附近公里排序的习惯,那附近的公里是怎么设计的呢?今天shigen带你一起揭秘。


分析


我们先明确一下需求,每个商家都有一个地址对吧,我们也有一个地址,我们点餐的时候,就是以我们自己所在的位置为圆心,向外辐射,这一圈上有一堆的商家。类似我下方的图展示:



想到了位置,我们自然想到了卫星定位,想到了二维的坐标。那这个需求我们有什么好的设计方案吗?


redis的GEO地理位置坐标这个数据结构刚好能解决我们的需求。


GEO


GEO 是一种地理空间数据结构,它可以存储和处理地理位置信息。它以有序集合(Sorted Set)的形式存储地理位置的经度和纬度,以及与之关联的成员。


以下是 Redis GEO 的一些常见操作:



  1. GEOADD key longitude latitude member [longitude latitude member ...]:将一个或多个地理位置及其成员添加到指定的键中。 示例:GEOADD cities -122.4194 37.7749 "San Francisco" -74.0059 40.7128 "New York"

  2. GEODIST key member1 member2 [unit]:计算两个成员之间的距离。 示例:GEODIST cities "San Francisco" "New York" km

  3. GEOPOS key member [member ...]:获取一个或多个成员的经度和纬度。 示例:GEOPOS cities "San Francisco" "New York"

  4. GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]:根据给定的经纬度和半径,在指定范围内查找与给定位置相匹配的成员。 示例:GEORADIUS cities -122.4194 37.7749 100 km WITHDIST COUNT 5


Redis 的 GEO 功能可用于许多应用场景,例如:



  • 位置服务:可以存储城市、商店、用户等位置信息,并通过距离计算来查找附近的位置。

  • 地理围栏:可以存储地理围栏的边界信息,并检查给定的位置是否在围栏内。

  • 最短路径:可以将城市或节点作为地理位置,结合图算法,查找两个位置之间的最短路径。

  • 热点分析:可以根据位置信息生成热力图,统计热门区域或目标位置的访问频率。


Redis 的 GEO 功能提供了方便且高效的方式来存储和操作地理位置信息,使得处理地理空间数据变得更加简单和快速。



默默的说一句,redis在路径规划下边竟然也这么厉害!



好的,那我们就来开始实现吧。今天我的操作还是用代码来展示,毕竟经纬度在控制台输入可能会出错。


代码实现


今天的案例是将湖北省武汉市各个区的数据存储在redis中,并以我所在的位置计算离别的区距离,以及我最近10km内的区。数据来源



我的测试代码如下,其中的运行结果也在对应的注释上有显示。



因为代码图片的宽度过长,导致代码字体很小,在移动端可尝试横屏观看;在PC端可尝试右键在新标签页打开图片。




以上的代码案例也参考:Redis GEO 常用 RedisTemplate API(Java),感谢作者提供的代码案例支持。


总结


对于需要存储地理数据和需要进行地理计算的需求,可以尝试使用redis进行解决。当然,elasticsearch也提供了对应的数据类型支持。有机会的话,shigen也会逐一的展开分析讲解。感谢伙伴们的支持。


shigen一起,每天不一样!


作者:shigen01
来源:juejin.cn/post/7275595571733282853
收起阅读 »

当以有涯之生,多行未做之事

造的轮子各种“抛锚”,心无旁骛“查案”自得其乐,日志一行行输出,紧盯屏幕唯恐遗漏任何细节。最后水落石出真相大白,揉搓酸痛睡眼,见有斑点血块。 明明心羡老庄,骑青牛西去,驾鲲鹏高飞---却只能在愈发稀少的酣眠中。身在凡尘结庐人境,无时不向往桃花源,又常自作五柳生...
继续阅读 »

造的轮子各种“抛锚”,心无旁骛“查案”自得其乐,日志一行行输出,紧盯屏幕唯恐遗漏任何细节。最后水落石出真相大白,揉搓酸痛睡眼,见有斑点血块。


明明心羡老庄,骑青牛西去,驾鲲鹏高飞---却只能在愈发稀少的酣眠中。身在凡尘结庐人境,无时不向往桃花源,又常自作五柳生,而被时代洪流,为资本巨手,被社会车轮惯性,夹持裹挟牢牢吞噬,活成了杰克伦敦及其马丁伊登。恐怖的是还打了鸡血般歆享,只能在夜深人静偶尔发现。


12年前那个闷热暑假,我读塞林格『麦田里的守望者』,不解其意味如嚼蜡。更对『Beat Generation』模糊朦胧,缺乏真切感知。多年后,似乎有一些懂了,却亦成为局中人。


再过100年,此刻的绝大多数生物体都将归为齑粉。而过后1000年,能将名姓遗物留存下的,能有几个?恐多不过一二。不惭自认好铁,不甘宰鸡屠狗,却也找不好方位,目不见睫皓首穷经。


每当这般忖夺,实觉人生之趣不过如此,牵绊者,唯父母亲友,连兴趣爱好。正是亲戚情话,琴书消忧。转念又想,更当以有涯之生,多行未做之事。这消弭了对挫折的痛楚,也减弱了于成功的喜悦,更在不仄足中,一直在奔跑,不太敢停歇。


“我们生活在一个不值得大师用文字记录的时代”,回溯展望,看似日新月异实则千篇一律。结婚生子,困于奶粉尿布,便无暇无心“胡思乱想”。子又生孙,孙又生子,总会有某些瞬间,踏进同一条河流。我想到了故事里,黄土坡的那个放羊娃。当略带讥嘻看完,心有庆幸,却无意识到,都大抵如此。…


幸甚是凡夫俗子,惦念着天亮的一顿饕餮,就能暂歇扫去千般愁绪。




作者:fliter
来源:juejin.cn/post/7277799132119629882
收起阅读 »

彻底搞懂小程序登录流程-附小程序和服务端代码

web
编者按:本文作者奇舞团高级前端开发工程师冯通 用户登录是大部分完整 App 必备的流程 一个简单的用户系统需要关注至少这些层面 安全性(加密) 持久化登录态(类似cookie) 登录过期处理 确保用户唯一性, 避免出现多账号 授权 绑定用户昵称头像等信息 ...
继续阅读 »

编者按:本文作者奇舞团高级前端开发工程师冯通



用户登录是大部分完整 App 必备的流程


一个简单的用户系统需要关注至少这些层面



  • 安全性(加密)

  • 持久化登录态(类似cookie)

  • 登录过期处理

  • 确保用户唯一性, 避免出现多账号

  • 授权

  • 绑定用户昵称头像等信息

  • 绑定手机号(实名和密保方式)


很多的业务需求都可以抽象成 Restful 接口配合 CRUD 操作


但登录流程却是错综复杂, 各个平台有各自的流程, 反倒成了项目中费时间的部分, 比如小程序的登录流程



对于一个从零开始的项目来说, 搞定登录流程, 就是一个好的开始, 一个好的开始, 就是成功的一半


本文就以微信小程序这个平台, 讲述一个完整的自定义用户登录流程, 一起来啃这块难啃的骨头


名词解释


先给登录流程时序图中出现的名词简单做一个解释



  • code 临时登录凭证, 有效期五分钟, 通过 wx.login() 获取

  • session_key 会话密钥, 服务端通过 code2Session 获取

  • openId 用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取

  • unionId 用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变

  • appId 小程序唯一标识

  • appSecret 小程序的 app secret, 可以和 code, appId 一起换取 session_key


其他名词



  • rawData 不包括敏感信息的原始数据字符串,用于计算签名

  • encryptedData 包含敏感信息的用户信息, 是加密的

  • signature 用于校验用户信息是否无篡改

  • iv 加密算法的初始向量



哪些信息是敏感信息呢? 手机号, openId, unionId, 可以看出这些值都可以唯一定位一个用户, 而昵称, 头像这些不能定位用户的都不是敏感信息



小程序登录相关函数



  • wx.login

  • wx.getUserInfo

  • wx.checkSession


小程序的 promise


我们发现小程序的异步接口都是 success 和 fail 的回调, 很容易写出回调地狱


因此可以先简单实现一个 wx 异步函数转成 promise 的工具函数


const promisify = original => {
return function(opt) {
return new Promise((resolve, reject) => {
opt = Object.assign({
success: resolve,
fail: reject
}, opt)
original(opt)
})
}
}

这样我们就可以这样调用函数了


promisify(wx.getStorage)({key: 'key'}).then(value => {
// success
}).catch(reason => {
// fail
})

服务端实现


本 demo 的服务端实现基于 express.js



注意, 为了 demo 的简洁性, 服务端使用 js 变量来保存用户数据, 也就是说如果重启服务端, 用户数据就清空了




如需持久化存储用户数据, 可以自行实现数据库相关逻辑



// 存储所有用户信息
const users = {
// openId 作为索引
openId: {
// 数据结构如下
openId: '', // 理论上不应该返回给前端
sessionKey: '',
nickName: '',
avatarUrl: '',
unionId: '',
phoneNumber: ''
}
}

app
.use(bodyParser.json())
.use(session({
secret: 'alittlegirl',
resave: false,
saveUninitialized: true
}))

小程序登录


我们先实现一个基本的 oauth 授权登录



oauth 授权登录主要是 code 换取 openId 和 sessionKey 的过程



前端小程序登录


写在 app.js 中


login () {
console.log('登录')
return util.promisify(wx.login)().then(({code}) => {
console.log(`code: ${code}`)
return http.post('/oauth/login', {
code,
type: 'wxapp'
})
})
}

服务端实现 oauth 授权


服务端实现上述 /oauth/login 这个接口


app
.post('/oauth/login', (req, res) => {
var params = req.body
var {code, type} = params
if (type === 'wxapp') {
// code 换取 openId 和 sessionKey 的主要逻辑
axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid: config.appId,
secret: config.appSecret,
js_code: code,
grant_type: 'authorization_code'
}
}).then(({data}) => {
var openId = data.openid
var user = users[openId]
if (!user) {
user = {
openId,
sessionKey: data.session_key
}
users[openId] = user
console.log('新用户', user)
} else {
console.log('老用户', user)
}
req.session.openId = user.openId
req.user = user
}).then(() => {
res.send({
code: 0
})
})
} else {
throw new Error('未知的授权类型')
}
})

获取用户信息


登录系统中都会有一个重要的功能: 获取用户信息, 我们称之为 getUserInfo


如果已登录用户调用 getUserInfo 则返回用户信息, 比如昵称, 头像等, 如果未登录则返回"用户未登录"



也就是说此接口还有判断用户是否登录的功效...



小程序的用户信息一般存储在 app.globalData.userInfo 中(模板如此)


我们在服务端加上前置中间件, 通过 session 来获取对应的用户信息, 并放在 req 对象中


app
.use((req, res, next) => {
req.user = users[req.session.openId]
next()
})

然后实现 /user/info 接口, 用来返回用户信息


app
.get('/user/info', (req, res) => {
if (req.user) {
return res.send({
code: 0,
data: req.user
})
}
throw new Error('用户未登录')
})

小程序调用用户信息接口


getUserInfo () {
return http.get('/user/info').then(response => {
let data = response.data
if (data && typeof data === 'object') {
// 获取用户信息成功则保存到全局
this.globalData.userInfo = data
return data
}
return Promise.reject(response)
})
}

专为小程序发请求设计的库


小程序代码通过 http.get, http.post 这样的 api 来发请求, 背后使用了一个请求库


@chunpu/http 是一个专门为小程序设计的 http 请求库, 可以在小程序上像 axios 一样发请求, 支持拦截器等强大功能, 甚至比 axios 更顺手


初始化方法如下


import http from '@chunpu/http'

http.init({
baseURL: 'http://localhost:9999', // 定义 baseURL, 用于本地测试
wx // 标记是微信小程序用
})

具体使用方法可参照文档 github.com/chunpu/http…


自定义登录态持久化


浏览器有 cookie, 然而小程序没有 cookie, 那怎么模仿出像网页这样的登录态呢?


这里要用到小程序自己的持久化接口, 也就是 setStorage 和 getStorage


为了方便各端共用接口, 或者直接复用 web 接口, 我们自行实现一个简单的读 cookie 和种 cookie 的逻辑


先是要根依据返回的 http response headers 来种上 cookie, 此处我们用到了 @chunpu/http 中的 response 拦截器, 和 axios 用法一样


http.interceptors.response.use(response => {
// 种 cookie
var {headers} = response
var cookies = headers['set-cookie'] || ''
cookies = cookies.split(/, */).reduce((prev, item) => {
item = item.split(/; */)[0]
var obj = http.qs.parse(item)
return Object.assign(prev, obj)
}, {})
if (cookies) {
return util.promisify(wx.getStorage)({
key: 'cookie'
}).catch(() => {}).then(res => {
res = res || {}
var allCookies = res.data || {}
Object.assign(allCookies, cookies)
return util.promisify(wx.setStorage)({
key: 'cookie',
data: allCookies
})
}).then(() => {
return response
})
}
return response
})

当然我们还需要在发请求的时候带上所有 cookie, 此处用的是 request 拦截器


http.interceptors.request.use(config => {
// 给请求带上 cookie
return util.promisify(wx.getStorage)({
key: 'cookie'
}).catch(() => {}).then(res => {
if (res && res.data) {
Object.assign(config.headers, {
Cookie: http.qs.stringify(res.data, ';', '=')
})
}
return config
})
})

登录态的有效期


我们知道, 浏览器里面的登录态 cookie 是有失效时间的, 比如一天, 七天, 或者一个月


也许有朋友会提出疑问, 直接用 storage 的话, 小程序的登录态有效期怎么办?


问到点上了! 小程序已经帮我们实现好了 session 有效期的判断 wx.checkSession


它比 cookie 更智能, 官方文档描述如下



通过 wx.login 接口获得的用户登录态拥有一定的时效性。用户越久未使用小程序,用户登录态越有可能失效。反之如果用户一直在使用小程序,则用户登录态一直保持有效



也就是说小程序还会帮我们自动 renew 咱们的登录态, 简直是人工智能 cookie, 点个赞👍


那具体在前端怎么操作呢? 代码写在 app.js 中


onLaunch: function () {
util.promisify(wx.checkSession)().then(() => {
console.log('session 生效')
return this.getUserInfo()
}).then(userInfo => {
console.log('登录成功', userInfo)
}).catch(err => {
console.log('自动登录失败, 重新登录', err)
return this.login()
}).catch(err => {
console.log('手动登录失败', err)
})
}

要注意, 这里的 session 不仅是前端的登录态, 也是后端 session_key 的有效期, 前端登录态失效了, 那后端也失效了需要更新 session_key



理论上小程序也可以自定义登录失效时间策略, 但这样的话我们需要考虑开发者自己的失效时间和小程序接口服务的失效时间, 还不如保持统一来的简单



确保每个 Page 都能获取到 userInfo


如果在新建小程序项目中选择 建立普通快速启动模板


我们会得到一个可以直接运行的模板


点开代码一看, 大部分代码都在处理 userInfo....



注释里写着



由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回




所以此处加入 callback 以防止这种情况



但这样的模板并不科学, 这样仅仅是考虑了首页需要用户信息的情况, 如果扫码进入的页面也需要用户信息呢? 还有直接进入跳转的未支付页活动页等...


如果每个页面都这样判断一遍是否加载完用户信息, 代码显得过于冗余


此时我们想到了 jQuery 的 ready 函数 $(function), 只要 document ready 了, 就可以直接执行函数里面的代码, 如果 document 还没 ready, 就等到 ready 后执行代码


就这个思路了! 我们把小程序的 App 当成网页的 document


我们的目标是可以这样在 Page 中不会出错的获取 userInfo


Page({
data: {
userInfo: null
},
onLoad: function () {
app.ready(() => {
this.setData({
userInfo: app.globalData.userInfo
})
})
}
})

此处我们使用 min-ready 来实现此功能


代码实现依然写在 app.js 中


import Ready from 'min-ready'

const ready = Ready()

App({
getUserInfo () {
// 获取用户信息作为全局方法
return http.get('/user/info').then(response => {
let data = response.data
if (data && typeof data === 'object') {
this.globalData.userInfo = data
// 获取 userInfo 成功的时机就是 app ready 的时机
ready.open()
return data
}
return Promise.reject(response)
})
},
ready (func) {
// 把函数放入队列中
ready.queue(func)
}
})

绑定用户信息和手机号


仅仅获取用户的 openId 是远远不够的, openId 只能标记用户, 连用户的昵称和头像都拿不到


如何获取这些用户信息然后存到后端数据库中呢?


我们在服务端实现这两个接口, 绑定用户信息, 绑定用户手机号


app
.post('/user/bindinfo', (req, res) => {
var user = req.user
if (user) {
var {encryptedData, iv} = req.body
var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
var data = pc.decryptData(encryptedData, iv)
Object.assign(user, data)
return res.send({
code: 0
})
}
throw new Error('用户未登录')
})

.post('/user/bindphone', (req, res) => {
var user = req.user
if (user) {
var {encryptedData, iv} = req.body
var pc = new WXBizDataCrypt(config.appId, user.sessionKey)
var data = pc.decryptData(encryptedData, iv)
Object.assign(user, data)
return res.send({
code: 0
})
}
throw new Error('用户未登录')
})

小程序个人中心 wxml 实现如下


<view wx:if="userInfo" class="userinfo">
<button
wx:if="{{!userInfo.nickName}}"
type="primary"
open-type="getUserInfo"
bindgetuserinfo="bindUserInfo">
获取头像昵称 </button>
<block wx:else>
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>

<button
wx:if="{{!userInfo.phoneNumber}}"
type="primary"
style="margin-top: 20px;"
open-type="getPhoneNumber"
bindgetphonenumber="bindPhoneNumber">
绑定手机号 </button>
<text wx:else>{{userInfo.phoneNumber}}</text>
</view>

小程序中的 bindUserInfo 和 bindPhoneNumber 函数, 根据微信最新的策略, 这俩操作都需要用户点击按钮统一授权才能触发


bindUserInfo (e) {
var detail = e.detail
if (detail.iv) {
http.post('/user/bindinfo', {
encryptedData: detail.encryptedData,
iv: detail.iv,
signature: detail.signature
}).then(() => {
return app.getUserInfo().then(userInfo => {
this.setData({
userInfo: userInfo
})
})
})
}
},
bindPhoneNumber (e) {
var detail = e.detail
if (detail.iv) {
http.post('/user/bindphone', {
encryptedData: detail.encryptedData,
iv: detail.iv
}).then(() => {
return app.getUserInfo().then(userInfo => {
this.setData({
userInfo: userInfo
})
})
})
}
}

代码


本文所提到的代码都可以在我的 github 上找到


小程序代码在 wxapp-login-demo


服务端 Node.js 代码在 wxapp-login-server


关于奇舞周刊


《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。



作者:奇舞精选
来源:juejin.cn/post/6844903702726180871
收起阅读 »

如何过完有效率的一周

时间管理有用 Or 无用? 上一篇面试反思被黄佬转载后,群里很多人反馈 时间利用率这么高是怎么做到的?难道我没有娱乐时间吗?为什么我自己就坚持不下去? 《如何过有效率的人生》给我埋下了种子 其实我也是受另外一个作者的影响,公众号:《如何过有效率的人生》,作者:...
继续阅读 »

时间管理有用 Or 无用?


上一篇面试反思被黄佬转载后,群里很多人反馈 时间利用率这么高是怎么做到的?难道我没有娱乐时间吗?为什么我自己就坚持不下去?


《如何过有效率的人生》给我埋下了种子


其实我也是受另外一个作者的影响,公众号:《如何过有效率的人生》,作者:“Abby”


这位作者是在两年前的内部分享中认识的(下面有链接),听完分享之后立马关注了公众号,那时我心理想的和你们一样,也是充满了各种疑惑。



幸运的是 这门课 被腾讯学堂开放出来了,大家都可以看看这套时间管理课程,相信我读完之后一定会有所收获。


鹅厂“高手在民间”| 互联网加班狗如何高效工作,腾出空,去生活?



《面试反思》让这颗种子发了芽




这不就是 时间管理吗,很自然的想起了abby老师 时间管理方法,于是 这次我决定跟上老师的步伐 给自己也制定一个时间管理的系统



如何入手时间管理?


思想认知跟上了,那么怎么做呢?


规划自己的人生系统



我们一生当中值得我们做的事情很少,大部分人都在浑浑噩噩浑然不知的情况下度过了一年又一年,没有成长也对生活生了倦怠



给自己留出 半个小时的时间,想想对于自己来说 哪些系统是有意义的, 写下来 这就是你的年度Flag。


也可以先 按照我的系统来 然后在做的过程中 找到对自己真正有意义感兴趣的系统。



人生系统要具体划分出来 任务细项


为什么要做这一步? 这涉及到决策疲劳,想想看 我们是不是决定做一件事情的时候 总是会被一些无关紧要的选择题消耗精力(读哪本书? 看哪个视频?)。



因此 我们要列出具体的任务事项,等到做事情的时候就不需要被选择消耗精力了,傻瓜式的执行就好了~~



人生系统如何划分 任务细项



任务纬度 是 人生系统里面很重要的指标,代表着某一个领域范围,详情内容是真正要执行的动作。



对于读书任务来说 任务是某一个领域的(认知/心理/理财/技术/,,,,,,),而详情内容就是 认知领域的数据有哪些,理财领域的书籍有哪些



强制规划自己的时间安排


为什么要做强制的时间规划



如果找不到目标或者一个做事习惯,那么就会浑浑噩噩的浪费掉时间。




这大概也是熵增定律吧哈哈,得有一个尺度去规范我们的时间管理,去制约我们的行为;不然时间就会 无意义的流逝掉。



曾经的我 也是 一个懒癌,荒废了很多时间,从我的个人介绍中 就能看出来。。。。哈哈哈



言归正传,时间管理 大家都知道 , 但是真正做的时候很难。为什么?也是上面的原因导致的:决策疲劳。能做的事情太多了, 这也想做,那也想做 在纠结中 浪费了很多时间~~~


不如就从现在做起, 给自己两小时的时间(我就是这样),梳理一下自己的时间安排。


当然也可以抄我的作业,后面根据你的 实际情况进行调整


我的工作日时间规划


上午下午

我的周末时间规划


周六

周日

月日历


工作日



周六


周日



“时间印迹”- 量化时间 让时间流逝的更有~意义


为什么要记录时间:优化之前先量化



人都有一种高估自己的心理,总觉得自己做完一件事情,应该花不了多长时间,其实记录过时间的人都知道,预估值和实际值往往差距还挺大,这也即著名的侯世达定律: *“实际做事花费的时间总是比预期的要长,即使预期中考虑了侯世达定律” *。



而通过记录时间,就可以让我们踏踏实实感受到自己的真实水平,而不是脱离实际,好高骛远。




但身处互联网时代,干扰我们注意力的事情太多太多,在纸上记录时间的切换成本很高,我曾尝试了好几次都坚持不下来。后来发现了这款用起来非常顺手的软件--时间印迹(IOS)。


记录时间的好处有哪些?



  1. 能够清楚的知道自己每类事情每天的花费占比多少,进而推算当天的价值有多大,是不是被浪费掉了;

  2. 感受到时间在滴滴答答的流逝,做事情更高效了;

  3. 预估时间更准了,比如:开发一个普通报表的时间,整个流程做完不超过半小时;完成一次好的PPT分享至少需要准备3天时间;

  4. 能更脚踏实地的踩稳今天了,深知:明天有明天的功课,今天的功课今天必须得完成


时间印迹-- 量化时间

怎样记录时间--轻轻一点“计时”


之前说到 量化时间,那么如何做呢?



首先记录自己当前每天的时间消耗状况(轻点计时),然后回顾自己一周的时间消耗,开始小步优化自己的时间安排



怎样记录时间--轻轻一点“计时”

让记录时间更有动力:热力图~


让记录时间更有动力:热力图~

时间管理成果展示:我的六大人生系统


读书系统


技术力:Flomo


技术力目前的学习领地 是在极客时间,极客时间中的笔记系统对于导出不太友好,所以我直接记录在了flomo中。


flomo热力图

通用力:语雀文档


关于技术力的东西 市面上有整个技术学习路线图辅助我 搭建个人技术能力框架,但是对于通用力来说范围太广,没有约定俗成的一种规范和标准,因此只能自己摸索。


好在最近阅读的“如何读书”专栏中的书 给了我一些启发,对于通用力的书籍如何记录笔记也有了些自己的框架。



高效阅读读书笔记


高效阅读笔记

如何阅读一本书 读书笔记


如何阅读一本书笔记


整理系统


时间印迹


记录只需要轻轻一点,目前一天总时间花费 大概在三分钟左右。



更方便的回顾查看自己过去一天过去一周,一月,一年的时间花费,目前终身会员有优惠,持续到十月八号,有兴趣的可以看看(独立开发者做的产品)。




飞书汇报


目前来说模板太长了哈哈哈,不过搭了一个框架之后需要填的内容很少。


日报目前可以 控制在十分钟以内




反思系统


飞书妙计


大致可以控制在十五分钟左右,以后要控制在十分钟以内。




作者:北洋
来源:juejin.cn/post/7276675247597305867
收起阅读 »

30岁之前什么新技术我都学,30岁之后什么新技术我都不学。

前言 今年是我步入程序员行业的第9年,大概到明年年后就满10年了。 有些唏嘘,嗯,又多干一年(又多活一年)。 现在依稀记得当年玩仙剑四最后慕容紫英说的那句话: 人生一场虚空大梦,韶华白首,不过转瞬。唯有天道恒在,循环往复,不曾更改。 现实生活的...
继续阅读 »

前言


1.jpeg



今年是我步入程序员行业的第9年,大概到明年年后就满10年了。




有些唏嘘,嗯,又多干一年(又多活一年)。




现在依稀记得当年玩仙剑四最后慕容紫英说的那句话:


人生一场虚空大梦,韶华白首,不过转瞬。唯有天道恒在,循环往复,不曾更改。




现实生活的冰冷和枯燥,大体也就这般了吧。



30岁之前


1、高考失利



2007年,我在全国高考失利了,总分469,理科生,在当时来讲,最多也就是二本二了。




而我在之前的大多数模拟考中,都是530分以上,我自认为是比较努力的那一类学生。




无数个日日夜夜,起早贪黑,一次考试,竹篮打水,前功尽弃。




遥想古代学子名落孙山的落魄心情,人生第一次脱离书本得到体会。




同时,高考的失利,让十八岁的我第一次明白,原来努力真不一定有用。



2、去当兵了



2007年左右参军的人,有不少是因为《士兵突击》而去的,唯一腔热血尔。




我不是。




我报了湖北第二师范大学,当年的分数勉强能过的,学校也打电话了。




我十几岁就是个对未来有些许规划的人,知道上不了理想中的大学,家庭经济也支撑不起复读一年,我立马就决定选专业为主,老师是我很早就在内心有计较的一个方向。




父母的反对是出乎我意料之外的,他们魔怔了,竟然让我去参军,进去考军校。




我愤怒且惊慌,大闹一场,事后得知是一位亲戚的怂恿,说是认识里面的人,可以走关系,也说我本身成绩不错,那几年去部队的很多都是小混混或者家里管不了的,文化水平很低,我这样的学生去了机会很大。




我的亲戚不多言,当时在创业做生意,到处筹钱,想让我父母抵押房子,父母认知浅,被忽悠了,中邪一样。




我是个孝顺的孩子,母亲血压一直不好,我妥协了,条件就是不许抵押房子给他们,否则一死了之。




事后证明我的强硬是对的,其他有些亲戚被骗了,父母才大梦初醒,我从懂事开始,一直在大事上从不糊涂。




去部队了,父母哭很惨,一个月后就后悔了,因为我是独子。




我在新兵连像行尸走肉一般熬了3个月,每天就三件事:跑步呕吐、单杠吊死猪、紧急集合。连吃饭和挨打都不在我的记忆中。




那个时候,在部队,城市兵是差农村兵很大一截的。



3、考军校?



当兵后悔两年,不当兵后悔一辈子,不知道现在部队还流行这句话不。




在部队,我最苦的日子回忆起来基本都是一群武汉兵带来的,他们很抱团,但是又很爱欺负同样是湖北的新兵,我给他们洗过沾屎的内裤,也被他们罚过吃没打扫干净的蜘蛛网。




到现在我都没明白为什么,而两广的兵、湖南的兵、江西的兵、温州的兵,都很团结,那两年义务兵,最照顾我的反而是广东的老兵。




虽然不提倡这个,但部队就是个大熔炉,抱团随处可见,湖北兵的尔虞我诈,是我心头至今的一片阴霾。




我真正适应部队生活是在一年后,成为了上等兵,人就是这样,改变不了,那就努力适应。




硬要说我当兵后对国家有什么贡献,大的没有,但赶上了2008年特大冰灾。




而我所在的部队驻地湖南,又是重灾区,什么郴州、衡阳很多地方都被砸烂了。




开个玩笑,如果我不做程序员了,我觉得体力活我还能捡回来一点,因为我的铁锹和镐头功力很深,都是在抗冰灾以及挖电缆沟的时候练出来的。




第二年开始,我体能达到了巅峰,一个山地5公里跑下来甚至没怎么喘气,对于从小体弱多病的我而言,倒是个惊喜,有种内功大成的感觉。




除此之外,也没有更多惊喜了,所谓考军校,名额是有的,但机会没有,因为有关系的兵是可以被安排到一些执勤点或哨所,安静的温习功课准备考试的,我这样的就一直训练、干活,想挑灯夜战连复习资料都没。




当年在部队还可以考军校,现在估计不行了,没有分数线,就是按分数从高到低录取一定的人头即可,对我来说确实很大机会。




我在部队没关系,家里也走不到关系,有关系的在新兵结束刚下连队就被安排去学开车、学叉车、学训犬、学修坦克(有一个战友学了但没留队是我心中一个未解之谜)之类的等等,我这种没关系的就在连队一直训练、出公差、偶尔种地养猪放牛,过着与世隔绝的田园生活。




考军校那只能是大梦一场。




终究是退伍了,也没什么留恋,但确实没后悔过,给了我一些很不同的人生经历和视角。



4、退伍了做什么



我有一群战友在部队欠了钱,退伍的时候小店老板在大门口拦着不让走,什么时候还钱什么时候走。




这算是意外留队了么……




后面他们电话找父母打钱过来才放走的,和我这种还攒了几千块的相比,确实谈不上光荣退伍了。




我带着一个嘉奖和一个优秀士兵的勋章回去了,父母凌晨在火车站看到我的那一刻喜极而泣,抱着我不撒手,我也对过往的事情释怀了。




当年,政策上更关照城市兵,退伍回来还有2万多的补贴,而农村兵没有。




我交出在部队攒的几千块钱,加上政府给的2万多补贴,给家里缓解了很大的压力,度过了那两年最艰难的时期,算是我成年后为数不多开心的事情。




至于接下来做什么,我有想过,继续上大学,但年纪大了,家里情况如此,我果断放弃了。




我选择了一个2010年左右逐渐新兴且我认为很有潜力的行业,动漫专业,也是我本身喜欢的东西,有了我前面的经历,我更清楚了,要做自己喜欢的事情,才能有动力去深耕,自己也会快乐。




父母总觉得亏欠我,所以什么都支持我,我理解但也不希望这样相处。




我兴致勃勃地报了这个学校,同时也报了成人大学,费用相对低一些,每年出一部分,对于全日制大学无望的我而言,这是最佳选择。




怀揣着异样的心情,我准备迎接新的人生起点。



5、误入编程行业


2.jpeg



我的人生在18-24岁的期间,都是颠簸的。




我选择的动漫专业在那一年只剩下建筑动漫,而我决定退而求其次的时候,这个专业也没了,因为和学校合作的一家深圳企业满额了。




我人都快昏了,这个时间点我还去哪里找专业,可以说已经没了。




再花时间去选择吗,我甚至厌恶了选择,我也没有那个条件选择,再拖一年看看?我本身就比别人更晚步入社会了。




我知道这一年我必须要入学,否则所有的计划都会被打乱。




编程行业是该校老师对我的建议,因为他们有华为回来的Java高级工程师做老师,还有拿到微软高级认证的专家授课。




什么华为?没听过,什么微软?修电脑的吗。




我当时大概就是这样的心情,入学测验体会了一把,那简直是颠覆了我的认知,那些代码就像蝌蚪文一样,多看一眼都浑身难受。




一直到截止报名的最后两天,我才终于咬牙进来了,再苦能比部队生活更艰苦吗?改造过的人,有军事素养,还怕学不会吗。




我本质上是一个误入编程行业的人,在入行前5年,我都没喜欢过这个行业。



6、Java和.Net选哪个



在2010年-2014年之间,我回头来看,Java是处在最大红利期的时候,但.Net也不不遑多让。




我在第一年的时候,是两个一起学的,.Net很舒适,各种拖控件,熟悉了之后那叫一个快,Java就枯燥多了,编码非常多,和现在比,以前真的很多。




HTML、CSS、JS、JSP、Struts2(Struts1即将淘汰)、Hibernate、Spring,再加上个三大数据库,那个时候的Java课程其实就这些东西。




第二年的时候我拿到了程序员资格证,也考过了.Net的很多微软认证,这个时候我才真正对编程有了一些入门的感觉,也就是知道它到底是个啥玩意儿了。




第三年的时候,参加了当时省内举办的一个编程赛事,还和同学一起写项目拿了个小奖,这算是对编程的实战有了新的体会。




编程最快乐的事情,即使到现在,也依然是产出成果的时候。




我最终还是选择了Java为主修语言,因为好找工作,就这么简单。




没想到的是,Java到现在,还是最好找工作的语言,这里面有太多戏剧性,但只能说明它确实厉害。



7、参加工作了



2014年是我参加工作的第一年,我选择了去广州,因为我在部队所属的军区就是广州军区。




我的心情是带着欢快和紧张的,我年纪其实不小了,和正规大学生比,我其实已经比他们晚了两年步入现实社会。




但找工作比我预想的容易很多,或者说太容易了,因为当时的IT公司除了大厂和事业单位,几乎是不看学历的,更看中你是否能马上开始干活。




简历刚投出去,就有三四个面试电话过来,凭着我当时的准备,很容易就过了两个,开价都是5000。




带队老师说我在学校的时候个人能力就比较强,让我这几天再试试多面几个,我答应了。




后面每天都有不少电话面试,我一共面了大概7个公司,最终选择了开价最高的那个6000的。




收到offer的时候我开心坏了,终于工作了,而且一个月6000,五险一金,试用期给80%,在当时入行的程序员里面算是挺高了。




其他同学还在苦苦面试,我顺便也给他们一些指导,睡觉前也在想拿到第一个月工资该怎么花。




那几年有两个培训机构其实很火,叫北大青鸟和清华IT,广告里面都是白领一样的老师和学生,令人眼馋。




而当我背着笔记本,胸前挂上工牌的那一刻,我真觉得自己是白领了。



8、工作和学习真不一样



我在第一家公司受了严重的打击,一度一蹶不振。




因为我明明在学校学了很多,也很优秀,但在公司,我是懵圈的。




业务读不懂,我连CRUD都步履维艰,一些没见过的技术,你不学会甚至没法干活。




在公司就是这样,你干不了活,就是不行,因为在学校你是付钱的客户,在公司,你只是拿钱的工人。




半年后,我的主管,一位湖北仙桃的半个老乡,对我有了很大的意见,甚至在办公室吼我。




我感觉丢脸极了,即使不抬头我也知道很多人都在看着我,这种无力的屈辱感如蚂蚁钻心一般。




我提离职了,才半年多,我扛不住了,选择了远离,主管很快就批了。




为什么我在外地对湖北人一直避之不及,除了在部队的时候经历过一次,就是这一次带来的心灵打击了。




在离职正式要走的那一天,我的主管湖北老乡来送我,他说了一句我现在都忘不了的话。




你可能不适合这个行业,可以考虑做点别的。




轰……




不是我一拳轰出打爆他脑袋哈,而是当时内心的感觉,头皮好像都要炸开了,我是失魂落魄走出公司回到出租屋里的。




接下来,我整整半年没找工作,靠着自己的一点钱,公司发的工资,以及同学的接济,勉强度过了半年。




为什么我不找了,因为内心害怕,害怕那种在公司里什么也不会,对着一个问题或BUG解决一天也解决不了,也不好意思总是问别人,那种每天只希望早点下班的煎熬、度日如年的感觉。




在学校,我很优秀,很努力,还拿过奖,工作后,我竟然一无是处,什么活也干不好。




工作和学习,真的不太一样。



9、编程是需要开窍的


3.jpeg



假装在外地工作不错,第一年回老家过年,父母听闻后洋溢着幸福的笑容。




在家的温暖和同学的鼓励下,我才重拾信心,年后又开始找工作。




这一次,我找到了一家传统行业的公司,给电网做服务的,薪水开了7500,比我预期要好很多。




有了前面的经历,这次我领悟了一些事情。




进了公司之后,我首先就请同组的同事和主管一起吃了个饭,还去了KTV,增进了一下感情。




大家觉得我很好相处,后面果然顺利很多,同事们都对我提供帮助,主管也经常问我有没有什么困难。




接下来的几个月,我慢慢熟悉了很多业务,做项目和日常维护也有了一些心得,每次处理一个问题,我都会记录下来,久而久之,我的积累甚至能比公司驻场的运维人员还强了。




传统企业对接接口是非常多的,主要是webservice,这是在学校老师不怎么教的,所以我根本不会。




正因为有了前面和同事打好关系,才有人愿意给我更多帮助,我才能掌握公司的很多常用技术,而这些都是你在一个公司的立身之本。




这种情况一直维持到了第二年,加起来是我工作3年的时候,我对很多脉络都理顺了,对编程的一些系统认识,以及项目在企业中的运作也了然于胸。




工作第3年,这是我第一次发现,我好像开窍了,忽然知道项目该怎么写了,接口该怎么写了,不需要问任何人,很奇妙的感觉。




现在的程序员刚工作就要收入过万,动辄就会分布式架构之类的,对我这种经历的程序员而言,我是理解不了的,我要到第3年才真觉得自己开窍了。




难道真的是时代变了。



10、传统行业能学到的也多



进什么行业,对刚入行的程序员来说,不是每个人都能做出选择的。




有不少程序员其实都进了传统行业,服务于政企、事业单位等等。




我这样在传统行业呆了5年的人,对于传统行业的看法也是能不进就不进,但没选择也别觉得它就差劲。




很多传统行业所用的技术其实是过时的,跟不上主流的,这和服务的对象有关,讲究求稳不求变。




但你千万别以为传统行业什么都学不到,我在前面提到的这家公司,主管曾经跟我说过一段话很有道理。




不管在什么行业,只要你肯学,你都能学到东西,你不肯学,在哪里都一样。




我在这家传统行业,学到了电力行业的诸多业务,驻场期间学到了其他厂家的一些技术,比如SOA的服务架构、dubbo、zookeeper、RabbitMQ、netty等技术,都是在此期间互相交流学到的。




同时,传统行业的工作比较有规律,只要掌握了以后,是有节奏的,你很好把控自己的时间,我在这5年期间,还通过摸鱼陆续学会了后面几年开始兴起的SpringBoot、SpringCloud、docker等技术。




如果是在互联网公司,它如果不采用某些技术,因为工作紧凑,加班也多,你是万万学不到的,只能靠自己挤时间。




这就是有得有失,你如果是在传统行业混日子,那确实白瞎,你如果利用好时间,搞不好3年时间下来你比互联网公司的一些程序员懂得的还多。




我就是在传统行业的这5年,凭借自学,薪水翻倍的,甚至在回老家之后,进入了一家互联网公司,也得心应手,成为了研发骨干。



30岁之后


1、30岁是个分水岭



我从不觉得35岁对于一个程序员是分水岭,反而觉得30岁才是。




对于普通人出身的程序员而言,30岁你将面临的最重要的一件事就是终身大事。




这是哪怕最终你可以逃掉,但过程你绝对逃不掉的人生转折点。




看过我以前文章的就知道,我个人是倾向于大家努力走入婚姻的,这不是强迫,而是陈述大自然的规律。




我在26岁的时候,家里已经开始稍微提一下谈朋友的事了,更多的是开玩笑的口吻。




在28岁的时候,他们的口吻已经略显严肃了,而我也试着相亲过两个。




没成功,更多的是尴尬,你要知道,相亲是要牵扯到第三方的,往往牵线的会是熟人或者你父母的熟人,你谈不好终归会让这里面的关系变的比正常关系微妙。




而29岁的时候,这种压力就真的逐渐步入顶峰,因为父母的传统思维依然是,过了30岁就不好找了。




所以你会面临除了工作以外的,来自家庭的关于个人问题的巨大压力。




尤其是程序员,很多还是不怎么会谈朋友的,工作加班、学新技术、看动漫、打游戏……如果再来个看综艺的习惯,你还有什么时间谈恋爱。




我很不幸,30岁依然没谈好,但我有别于许多自暴自弃顺其自然的人,我在30岁那年做了一件没有后悔的事。




我在薪水翻倍,并且即将升任新小组的研发主管的这一年,选择了离职回老家。




公司有些惊讶,试图挽留过,但因为牵扯到我的个人问题,他们最终不好说什么。




所以,在广州摸爬滚打混迹6年的我,提着一个包来到广州,最后又提着一个包离开广州。




原来人生做出选择没那么难,就是一个包的事情。



2、相亲是个体力活


4.jpeg



回到老家是2019年,幸运的是,年底就开始传出疫情,第二年年初就爆发了。




如果我没有鬼使神差的回来,我估计很长时间都回不来了,可能还会让父母急到崩溃。




我很顺利的在老家找到了一家互联网公司,是个小公司,给我开了6.5k,目前凭着我的努力已经涨到了7.5k。




和在外地相比,薪水何止腰斩,一度让我很不习惯,但我所得也很多,吃穿有家人更多照料了,没有广州那种湿热的环境,身体也逐渐变好了。




果然,身体的健康有很大一部分来源于心情。




工作的问题解决了,相亲就是个难题了。




我回忆起来,大概相亲了不止10个,按照当初我母亲的斥责:你相亲的对象都能凑几桌麻将了。




很累,相亲真的很累,不止身体,还有心理。




要么你看不上别人,要么别人看不上你。我对此的形容是,不分男女,大家都有问题,就像一堆烂萝卜,最后都剩在一个框里,还要让这框里的烂萝卜成双成对。




这简直是双倍难度……




我能坦然接受相亲也是有过长足的心理建设的,我年轻点的时候始终希望,我的意中人有一天会驾着七彩……哦不对,我的意中人一定要是我喜欢的,否则宁愿不谈。




想法没错,但想要走入婚姻,那确实得想开点,否则犹如大海捞针,真心随缘了,和尚都不会这么干。




我坚持下来后,32岁这年终究是修成正果,其中我付出的努力和艰辛不足为外人道也,如果你不愿意在这个年纪为这件事主动付出更多,以现在的社会情况,那确实很难成家。




我有了一个谈不上特别喜欢,但确实相互合适的另一半,33岁小孩出生,目前也一岁多了,相处这几年下来,夫妻也算挺和睦,家中矛盾不能说没有,总体和我预想的生活差别不大。




重要的是,有了家庭,步入了婚姻,哪怕琐事缠身,但内心已然安定,对未来没有孤独感,可以明确下一步的方向,努力在事业上投入更多心神而不被人生大事所干扰、打乱。




我想古人说的三十而立,可能包含这个意味在里面,这是我30岁之前不可能理解的。



3、30岁之后的程序员



我在30岁之前,学了非常多的技术,最离谱的时候,我花钱买服务器来搭建大型分布式架构,以帮助我提升自己的专业能力。




回到老家之后,我进入了一家互联网医疗行业,涵盖的业务范围还挺广,包含了智慧医院、互联网医疗、统一支付平台、医保支付、动账消息推送等等。




像一些动账消息,一月会有几次需要当日推送百万消息通知,比不了大厂,但在二三线城市算是有一定技术能力的企业了。




技术虽然掌握的挺多,但30岁之后如果一直徘徊于此,就国情而言,这样的程序员应该还是走错了。




这些年我写的接口有多少已经数不清了,看过烂到流脓的代码,也看过美丽如画的代码。




这里面就我知道的一些同事和同学,写过美丽如画代码的,有部分可能已经没干了,而写过烂到流脓代码的人,还有依然坚守在岗位上,甚至混的越来越好的那种。




我有一起吃饭交流过,算是琢磨出一点感悟来。




30岁之后,技术固然重要,人情世故可能更重要。




有些人,技术也许一般,也就是能干活的程度,但不妨碍他认识的人多,朋友多,有些程序员就喜欢这样的人,好相处,好沟通,平时乐于助人,也爱一起出来玩、吹牛皮。




至于技术上的问题,如果同事不再是同事,而是朋友,帮你解决真不是什么难事。



4、资源共享很重要



我目前这家互联网公司,在疫情爆发的2020年初,因为公司是医疗行业,必须在家熬夜支撑,甚至6天内就要出一个线上运行的项目,帮医生和患者们排忧解难。




我的主管也是刚来没多久的一个高级工程师,临危受命,带领我们几个一起做。




他规划和安排了一些事情,有些其实我感到不合理,但别人不说,我自认为有些经验,会据理力争。




到了这个年纪的程序员,都会有这样的通病,谁也不服谁。




这也给我后面的祸患埋下了一些伏笔。




矛盾爆发在初五的凌晨,我在微信通话中抱怨了几句,他没有说什么。




但第二天开始,什么活也不给我安排了,直到项目紧急上线,后面依然也没安排,我就知道自己得罪人了。




果不其然,因为疫情都在家里,公司要求每天报备,然后每周在一起钉钉群通话,每个人说自己这周干了些什么。




我贼尴尬,活都没安排,我说什么,之前还硬气呢,不安排干活我乐得清闲,原来这是给我摆了一道。




一周后,他又给我重新安排事情做了,此时我完全接受安排,一句抱怨都没了,大家心知肚明也都没主动提这茬,仍然正常交流。




后面我才知道,这人居然还是总经理从武汉请回来的一个大佬,技术很厉害,管理能力也很强。




接下来几年,我确实从他身上学到了非常多的东西,尤其是底层的一些原理,以前一知半解,现在茅塞顿开。




从那次矛盾之后,我就很主动的和他搞好关系,慢慢从他那里了解到一件重要的事情。




他已经是38岁的程序员,他的朋友圈有非常多很厉害的同行,有他以前的同学,也有他的前同事,还有他这些年网上认识的一些技术圈的人。




他说了一句话我感觉非常好,大概意思如下。




“现在是信息共享,资源共享的时代,程序员要学会扩大交际圈,得到更多可以共享的资源。”




他说他为什么能在疫情这么艰难的时期能进入到这家公司还成为技术主管,就是因为前两年结识了现在的总经理,才会有这样的机会。




除此以外,他能接收到的圈内资讯也比我快,遇到棘手的问题时,能咨询的圈内朋友也多,换成我,只能硬着头皮找BUG。




他还一直和很多同学保持着密切联系,这些同学在北上广都有,目前有的也是技术骨干了,他之前还把自己亲戚的小孩推荐给了上海一个同学所在的公司,不得不说这人脉令人佩服。




我是被深深震撼了,如果我不是运气好,我根本没机会进来,可能还在到处奔波,而他因为圈子广,比别人有更多找到工作的机会,一个电话就来了。




这些道理,我30岁之前连想的意思都没有,只是一门心思埋头苦学,追求新技术。




可技术年年都有新的,你学的完吗?你学不完。




人脉年年也能有新的,积累的越多,资源越多,你主动积累过吗?你没有,这对你来说可都是隐形财富。



5、不要贴35岁标签


5.jpeg



35岁的标签谁给你贴的?




是社会吗,是技术圈子吗,是公司和企业吗?他们只占一半,剩下一半是你自己贴的。




如果20-25是一个程序员入门编程行业的时期,那么26-30就是一个程序员技术能力爆发的时期。




这个时期有小部分程序员会主动加强对未来道路的铺垫和积累,这小部分人我是佩服的。




比如我入驻CSDN已经12年了,去年我偶然看到一个和我同年加入CSDN的博主,如今已经是知名大博主了,并且形成了自己的矩阵,有了庞大的粉丝关注。




比如天蚕土豆大火了这么多年,而我其实就在他后面一两年开始写小说,写的时候《斗破苍穹》还没火出圈,遗憾的是,我把当初的小白文红利期放弃了。




我年轻时做什么都喜欢半途而废,不是我意识不到,而是我控制不了,有多少人也是这样?




从学习算起,我入行编程领域是2010年,如果早点就开始有意识的积累,现在估计也有庞大的圈内资源了。




从写小说算起,我如果当时坚持下来,趁着小白文红利期,估计现在开新书也有一批死忠粉了。




类似的事情,我经历过好多次。




对于绝大部分程序员来说,像我这样的一类人终究是占比更多的。




大家更愿意把20-30岁的时光贡献给数之不尽的新技术,用以维持在编程领域的水平。




这无可厚非,精力有限,想要技术更厉害,保持学习是唯一路径。




可30岁之后,这种思维要主动求变了,继续保持学习固然重要,侧重点也许需要调整。




像我,30岁之后很少学习新技术,只是保持关注,但我拥有快速掌握新技术的能力,所以心里不慌。




30岁之前的程序员,如果你没有在这个阶段拥有这个能力,那么你这个阶段我认为是失败的。




30岁之后,需要开始多结交朋友,拓展资源,你入驻的平台是资源,你的朋友圈是资源,你待过的公司更是重要的资源。




你仔细想想,你待过的公司,那些同事你有好好维系关系吗,研发同事你还有联系吗,人事你还有联系吗,商务部销售部的同事你有联系吗,运维同事你有联系吗。




其次,你作为大学生,如果是科班生,你的同学大部分都应该是同行业的,你还都有联系吗,有互相交换信息交换资源吗?也许你26岁之前还有,但快30岁或者30岁之后就逐渐淡了。




为什么淡了,因为你自己的事情多了,就没有主动维系了,这是你的资源,你都不维系,指望资源缠着你吗。




大部分程序员是肯定没有考虑这些的,而等到30岁之后才想起,会留下很多遗憾,这都是喂到你嘴里你都不要的财富。




我鼓励30岁之后的程序员,在有了家庭以后,把更多精力放在周围看得见摸得着的资源和人际关系上,绝不能继续陷入追求新技术的泥沼中。




35岁的标签其实在30岁就已经决定了,5年时间足够你取下这个标签。




我认为,这是中国式程序员需要面临和沉思的课题。



总结



我一开始是有大纲的,这是以前写小说的习惯,大概就规划了3000字左右。




但是写到中途我发现,像我这样比上不齐,比下有余的程序员,应该是挺多的,毕竟我周围就有。




越写越觉得内心有一丝渴望得到救赎的悸动。




所以像我这样一群已经过了30岁的程序员,他们的生活和经历,是值得解析的,对于年轻点的程序员是有借鉴意义的。




因此我修改了大纲,加入了个人从成年后到目前为止的简要经历,每个阶段给了一些心理路程。




希望这种步入编程行业的经历,能够对仍旧或热爱、或挣扎、或迷茫的同行业者们一点正面积极的思考。




如果喜欢,可以点赞收藏关注,持续分享各种干货。


作者:程序员济癫
来源:juejin.cn/post/7274149231367454739
收起阅读 »

Bun 1.0 正式发布,爆火的前端运行时,速度遥遥领先!

web
9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和Type...
继续阅读 »

9 月 8 日,前端运行时 Bun 1.0 正式发布,如今,Bun 已经稳定并且适用于生产环境。Bun 不仅是一个专注性能与开发者体验的全新 JavaScript 运行时,还是一个快速的、全能的工具包,可用于运行、构建、测试和调试JavaScript和TypeScript代码,无论是从单个文件还是完整的全栈应用。
image.png
2022年,Bun 发布,随即爆火,成为年度最火的前端项目:
image.png
Bun 的流行程度伴随着在去年夏天发布的第一个 Beta 版而爆炸性增长:仅一个月内,就在 GitHub 上获得了超过两万颗 Star。
star-history-202397.png



Bun 不仅仅是一个运行时。它也是:



  • 一个包管理器 (类似 Yarn、 NPM、 PNPM)

  • 一个构建工具 (类似 Webpack、 ESBuild、 Parcel)

  • 一个测试运行器

  • ...


所以 Bun 可以通过读取 package.json 来安装依赖项。Bun 还可以运行脚本。不管它做什么都比其他工具更快。Bun 在 JavaScript 生态系统的许多方面都有新的尝试,其中的重点是性能。它优先支持标准的 Web API,如 Fetch。它也支持许多 Node.js APIs,使其能与大多数 NPM 包兼容。



安装 Bun:


// npm
npm install -g bun

// brew
brew tap oven-sh/bun
brew install bun

// curl
curl -fsSL https://bun.sh/install | bash

// docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

更新 Bun:


bun upgrade

下面就来看看 Bun 是什么,1.0 版本带来了哪些更新!


Bun:全能的工具包


JavaScript 成熟、发展迅速,并且有着充满活力和激情的开发者社区。然而,自14年前Node.js发布以来,JavaScript 的工具链变得越来越庞大和复杂。这是因为在发展过程中,各种工具被逐渐添加进来,但没有一个统一的集中规划,导致工具链缺乏整体性和效率,变得运行缓慢和复杂。


Bun 为什么会出现?


Bun的目标很简单,就是要消除JavaScript工具链的缓慢和复杂性,但同时保留JavaScript本身的优点。Bun希望让开发者继续使用喜欢的库和框架,并且无需放弃已经熟悉的规范和约定。


为了实现这个目标,可能需要放弃一些在使用Bun之后变得不再必要的工具:



  • Node.js:Bun 的一个可以直接替代的工具,因此不再需要以下工具:

    • node

    • npx:Bun 的 bunx 命令比 npx 快5倍。

    • nodemon:Bun 内置了监听模式,无需使用 nodemon

    • dotenvcross-env:Bun 默认支持读取.env文件的配置。



  • 转译器:Bun 可以运行.js.ts、``.cjs.mjs.jsx.tsx文件,因此不再需要以下工具:

    • tsc:仍然可以保留它用于类型检查!

    • babel.babelrc@babel/preset-*:不再需要使用 Babel 进行转译。

    • ts-nodets-node-esm:Bun 可以直接运行 TypeScript 文件。

    • tsx:Bun可以直接运行 TypeScript 的 JSX 文件。



  • 构建工具:Bun 具有一流的性能和与esbuild兼容的插件API,因此不再需要以下工具:

    • esbuild

    • webpack

    • parcel, .parcelrc

    • rollup, rollup.config.js



  • 包管理器:Bun 是一个与 npm 兼容的包管理器,可以使用熟悉的命令。它可以读取 package.json文件并将依赖写入node_modules目录,与其他包管理器的行为类似,因此可以替换以下工具:

    • npm, .npmrc, package-lock.json

    • yarn,yarn.lock

    • pnpm, pnpm.lock, pnpm-workspace.yaml

    • lern



  • 测试库:Bun是一个支持Jest的测试运行器,具有快照测试、模拟和代码覆盖率等功能,因此不再需要以下测试相关的工具:

    • jest, jest.config.js

    • ts-jest, @swc/jest, babel-jest

    • jest-extended

    • vitest, vitest.config.ts




尽管这些工具都有自己的优点,但使用它们时往往需要将它们全部集成在一起,这会导致开发过程变得缓慢和复杂。而Bun通过成为一个单一的工具包,提供了最佳的开发者体验,从性能到API设计都力求做到最好。


Bun:JavaScript 运行时


Bun是一个快速的JavaScript运行时。旨在提供出色的性能和开发体验。它的设计旨在解决开发过程中的各种痛点,使开发者的工作更加轻松和愉快。


与Node.js兼容


Bun 是可以直接替代 Node.js 的。这意味着现有的 Node.js 应用和 npm 包可以在 Bun 中正常工作。Bun 内置了对 Node.js API 的支持,包括:



  • 内置模块,如fspathnet

  • 全局对象,如__dirnameprocess

  • Node.js 模块解析算法(例如node_modules


尽管与 Node.js 完全兼容是不可能的,特别是一些依赖于v8版本的特性,但 Bun 几乎可以运行任何现有的 Node.js 应用。


Bun经过了与最受欢迎的Node.js包的兼容性测试,支持与Express、Koa、Hapi等服务端框架以及其他流行的全栈框架的无缝集成。开发者可以放心地在Bun中使用这些库和框架,并享受到更好的开发体验。
image.png
使用Next.js、Remix、Nuxt、Astro、SvelteKit、Nest、SolidStart和Vite构建的全栈应用可以在Bun中运行。


速度


Bun的速度非常快,启动速度比 Node.js 快 4 倍。当运行TypeScript文件时,这种差异会更加明显,因为在Node.js中运行TypeScript文件需要先进行转译才能运行。
image.png
Bun在运行一个简单的"Hello World" TypeScript文件时,比在Node.js中使用esbuild运行速度快5倍。


Bun使用的是Apple的WebKit引擎,而不是像Node.js和其他运行时一样使用Google的V8引擎。WebKit引擎是Safari浏览器的核心引擎,每天被数十亿的设备使用。它经过了长时间的实际应用和测试,具备快速和高效的特性。


TypeScript 和 JSX 支持


Bun内置了JavaScript转译器,因此可以运行JavaScript、TypeScript甚至JSX/TSX文件,无需任何依赖。


// 运行 TS 文件
bun index.ts

// 运行 JSX/TSX 文件
bun index.tsx

ESM 和 CommonJS 兼容


从CommonJS到ES模块的过渡一直是缓慢而充满挑战的。在引入ESM之后,Node.js花了5年时间才在没有--experimental-modules标志的情况下支持它。尽管如此,生态系统仍然充斥着CommonJS。


Bun 同时支持这两种模块系统。无论是使用CommonJS的.js扩展名、.cjs扩展名,还是使用ES模块的.mjs扩展名,Bun都会进行正确的解析和执行,而无需额外的配置。


甚至可以在同一个文件中同时使用importrequire()


import lodash from "lodash";
const _ = require("underscore");

Web API


Bun 内置支持浏览器中可用的Web标准API,如fetchRequestResponseWebSocketReadableStream等。


const response = await fetch("https://example.com/");
const text = await response.text();

开发者不再需要安装像node-fetchws这样的包。Bun内置的 Web API 是使用原生代码实现的,比第三方替代方案更快速和可靠。


热重载


Bun提供了热重载功能,可以在开发过程中实现文件的自动重新加载。只需在运行Bun时加上--hot参数,当文件发生变化时,Bun 就会自动重新加载你的应用,从而提高开发效率。


bun --hot server.ts

与像nodemon这样完全重新启动整个进程的工具不同,Bun 在重新加载代码时不会终止旧进程。这意味着HTTP和WebSocket连接不会断开,并且状态不会丢失。
hot (1).gif


插件


Bun 被设计为高度可定制的。
可以定义插件来拦截导入操作并执行自定义的加载逻辑。插件可以添加对其他文件类型的支持,比如.yaml.png。插件API的设计灵感来自于esbuild,这意味着大多数esbuild插件在 sBun 中也可以正常工作。


import { plugin } from "bun";

plugin({
name: "YAML",
async setup(build) {
const { load } = await import("js-yaml");
const { readFileSync } = await import("fs");
build.onLoad({ filter: /.(yaml|yml)$/ }, (args) => {
const text = readFileSync(args.path, "utf8");
const exports = load(text) as Record<string, any>;
return { exports, loader: "object" };
});
},
});

Bun API


Bun内部提供了针对开发者最常用需求的标准库API,并对其进行了高度优化。与Node.js的API不同,Node.js的API存在着向后兼容的考虑,而Bun的原生API则专注于提供更快速和更易于使用的功能。


Bun.file()


使用Bun.file()可以懒加载位于特定路径的文件。


const file = Bun.file("package.json");
const contents = await file.text();

它返回一个扩展了 Web 标准FileBunFile对象。文件内容可以以多种格式进行懒加载。


Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
});

Bun每秒可以处理的请求比 Node.js 多 4 倍。


也可以使用tls选项来配置TLS(传输层安全协议)。


Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
tls: {
key: Bun.file("/path/to/key.pem"),
cert: Bun.file("/path/to/cert.pem"),
}
});

Bun内置了对WebSocket的支持,只需要在websocket中定义一个事件处理程序来实现同时支持HTTP和WebSocket。而Node.js没有提供内置的WebSocket API,所以需要使用第三方依赖库(例如ws)来实现WebSocket的支持。因此,使用Bun可以更加方便和简单地实现WebSocket功能。


Bun.serve({
fetch() { ... },
websocket: {
open(ws) { ... },
message(ws, data) { ... },
close(ws, code, reason) { ... },
},
});

Bun 每秒可以处理的消息比在 Node.js 上使用 ws 库多 5 倍。


bun:sqlite


Bun内置了对 SQLite 的支持。它提供了一个受到better-sqlite3启发的API,但是使用本地代码编写,以达到更快的执行速度。


import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Bun' as runtime;");
query.get(); // => { runtime: "Bun" }

在 Node.js 上,Bun 执行 SQLite 查询操作的速度比better-sqlite3快 4 倍。


Bun.password


Bun 还支持一些常见但复杂的API,不用自己去实现它们。


例如,可以使用Bun.password来使用bcryptargon2算法进行密码哈希和验证,无需外部依赖。


const password = "super-secure-pa$$word";
const hash = await Bun.password.hash(password);
// => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

const isMatch = await Bun.password.verify(password, hash);
// => true

Bun:包管理器


Bun是一个包管理器。即使不使用Bun作为运行时环境,它内置的包管理器也可以加速开发流程。以前在安装依赖项时需要盯着npm的加载动画,现在可以通过Bun的包管理器更高效地进行依赖项的安装。


Bun可能看起来像你熟悉的包管理器:


bun install
bun add <package> [--dev|--production|--peer]
bun remove <package>
bun update <package>

安装速度


Bun的安装速度比 npm、yarn 和 pnpm 快好几个数量级。它利用全局模块缓存来避免从npm注册表中重复下载,并使用每个操作系统上最快速的系统调用。
image.png


运行脚本


很可能你已经有一段时间没有直接使用 Node 来运行脚本了。相反,通常使用包管理器(如npm、yarn等)与框架和命令行界面(CLI)进行交互,以构建应用。


npm run dev

你可以用bun run来替换npm run,每次运行命令都能节省 150 毫秒的时间。


这些数字可能看起来很小,但在运行命令行界面(CLI)时,感知上的差异是巨大的。使用"npm run"会明显感到延迟:
265893417-fbfb4172-5a91-4158-904f-55f2dbb0acde.gif而使用bun run则感觉几乎瞬间完成:
image.png
并不只是针对npm进行比较。实际上,bun run <command>的速度比yarn和pnpm中相应的命令更快。


脚本运行平均时间
npm run176ms
yarn run131ms
pnpm run259ms
bun run7ms 🚀

Bun:测试运行器


如果你以前在 JavaScript 中写过测试,可能了解 Jest,它开创了“expect”风格的API。


Bun有一个内置的测试模块bun:test,它与Jest完全兼容。


import { test, expect } from "bun:test";

test("2 + 2", () => {
expect(2 + 2).toBe(4);
});

可以使用bun test命令来运行测试:


bun test

还将获得 Bun 运行时的所有优势,包括TypeScript和JSX支持。


从Jest或Vite迁移很简单。@jest/globalsvitest的任何导入将在内部重新映射到bun:test,因此即使不进行任何代码更改,一切也将正常运行。


import { test } from "@jest/globals";

describe("test suite", () => {
// ...
});

在与 zod 的测试套件进行基准测试中,Bun比Jest快13倍,比Vite快8倍。
image.png
Bun的匹配器由快速的原生代码实现,Bun中的expect().toEqual()比Jest快100倍,比Vite快10倍。


可以使用bun test命令来加快 CI 构建速度,如果在Github Actions中,可以使用官方的oven-sh/setup-bun操作来设置Bun


name: CI
on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: oven-sh/setup-bun@v1
- run: bun test

Bun会自动为测试失败的部分添加注释,以便在持续集成(CI)日志中更容易理解。这样,当出现测试失败时,可以直接从日志中读取Bun提供的注释,而不需要深入分析代码和测试结果,从而更方便地检查问题所在。
image.png


Bun:构建工具


Bun是一个JavaScript和TypeScript的构建工具和代码压缩工具,可用于将代码打包成适用于浏览器、Node.js和其他平台的形式。


bun build ./index.tsx --outdir ./build

Bun 受到了 esbuild 的启发,并提供了兼容的插件API。


import mdx from "@mdx-js/esbuild";

Bun.build({
entrypoints: ["index.tsx"],
outdir: "build",
plugins: [mdx()],
});

Bun 的插件 API 是通用的,这意味着它适用于打包工具和运行时。所以前面提到的.yaml插件可以在这里使用,以支持在打包过程中导入.yaml文件。


根据esbuild的基准测试,Bun比esbuild快1.75倍,比Parcel 2快150倍,比Rollup + Terser快180倍,比Webpack快220倍。
image.png
由于Bun的运行时和打包工具是集成在一起的,这意味着Bun可以做其他打包工具无法做到的事情。


Bun引入了JavaScript宏机制,可以在打包时运行JavaScript函数。这些函数返回的值会直接内联到打包文件中。


// release.ts
export async function getRelease(): Promise<string> {
const response = await fetch(
"https://api.github.com/repos/oven-sh/bun/releases/latest"
);
const { tag_name } = await response.json();
return tag_name;
}

// index.ts
import { getRelease } from "./release.ts" with { type: "macro" };

// release的值是在打包时进行评估的,并且内联到打包文件中,而不是在运行时执行。
const release = await getRelease();

bun build index.ts
// index.ts
var release = await "bun-v1.0.0";

Bun:可以做更多事


Bun 在 macOS 和 Linux 上提供了原生构建支持,但 Windows 一直是一个明显的缺失。以前,在 Windows 上运行 Bun 需要安装 Windows 子系统来运行Linux系统,但现在不再需要。


Bun 首次发布了一个实验性的、专为Windows平台的本地版本的 Bun。这意味着Windows用户现在可以直接在其操作系统上使用 Bun,而无需额外的配置。
image.png
尽管Bun的macOS和Linux版本已经可以用于生产环境,但Windows版本目前仍然处于高度实验阶段。目前只支持JavaScript运行时,而包管理器、测试运行器和打包工具在稳定性更高之前都将被禁用。性能方面也还未进行优化。


Bun:面向未来


Bun 1.0 只是一个开始。Bun 团队正在开发一种全新的部署JavaScript和TypeScript到生产环境的方式,期待 Bun 未来更好的表现!


作者:CUGGZ
来源:juejin.cn/post/7277387014046335010
收起阅读 »

旅行的意义

写在前面 马上就到国庆了,估计不少朋友都有出去旅行的计划,曾几何时,我还是个“网瘾少年”,旅行在我眼里毫不夸张的讲就是浪费时间。随着年岁的增长,也慢慢开始主动或被动的出去走走,现在想想大多都是火车模式:“逛吃逛吃”,虽然好像也挺乐呵,但是感觉也只是在跟着人潮往...
继续阅读 »

写在前面


马上就到国庆了,估计不少朋友都有出去旅行的计划,曾几何时,我还是个“网瘾少年”,旅行在我眼里毫不夸张的讲就是浪费时间。随着年岁的增长,也慢慢开始主动或被动的出去走走,现在想想大多都是火车模式:“逛吃逛吃”,虽然好像也挺乐呵,但是感觉也只是在跟着人潮往前走,并没有驻足思考过为什么要去到那里,前段时间偶然开始思考旅行的意义这个命题,而这次我好像找到了我内心的答案。


缘起


之前有段时间感觉自己工作状态不是很好,正好系统也在提醒我有几天年假快过期了,于是就请了两天假,加上周末凑了个4天小长假,打算出去玩一玩,来一场说走就走的旅行。


说干就干,按照喜好挑选候选目的地、规避当时风头正劲的台风影响确定行程、查阅资料做好了几天攻略、订车票/酒店、收拾行李,一气呵成。当我拎包正要出门时,脸上的口罩提醒了我,现在还还是疫情时期(得益于广州市政府和防疫人员的付出,今年广州上半年疫情控制的比较好,所以我已经很久没有那种被疫情支配的恐惧了,听我说,谢...),我赶紧查了下目的地的疫情风险等级,结果是属于中高风险区,而且令人沮丧的是几个想去的候选目的地都或多或少都有疫情。


最终经过深思熟虑我放弃了这次“说走就走的旅行”,往大了说是此行可能有碍国家疫情防控大局,往小了说就是我还要保住这份工🐶。


短短两个小时,我的心情像过山车从高到低再到平静,正如尼布尔的祈祷文:“上帝,请赐予我平静,去接受我无法改变的;给予我勇气,去改变我能改变的;赐我智慧,分辨这两者的区别。”,有些事情显然不是我们能控制和改变的,我逐渐接受了我无法去到目的地旅行的事实并恢复平静。


平静后我开始思考,我以前好像并不是一个热爱旅行的人,为什么这次却“火急火燎”的想出去?旅行的意义是什么?


旅行是为了____


旅行是为了看美景,尝美食,这可能是最简单纯粹的理由了,去到不同的地方品尝各地美食,这是一名吃货的自我修养。虽说国内各种所谓网红景区如今已是商业化严重,卖着琳琅满目却同是来自义乌的小商品,各地小吃街好像也都在卖着类似的烤面筋、臭豆腐和大鱿鱼。


旅行是为了了解世界,开阔眼界,正如老话说的读万卷书,行万里路,能开阔眼界,增加阅历是极好的,但是后来我发现这也是需要一定人文素养门槛的,比如去长城如果不了解背后的历史事件和故事,只是走马观花,那回来也许只能感叹:“长城啊,真***长!”


旅行是为了逃离和放松,每个人都有可能在工作或生活中遭遇挫折,尤其是近几年随着各种所谓“红利”基本到头,本人所在的互联网行业势头逐渐放缓,叠加一些不可抗力因素影响,致使“寒冬”在今年春夏提前到来,寒气也确确实实传给了每一个人。有时候遇到一些无力改变的坏事确实会让人心情很 down,此时来一次旅行,换一个环境,换一种心情,也不失为一剂良药,毕竟心情变好了,事情才能变好。


...


还有很多其他理由,旅行是为了拍照发朋友圈、为了交友等等。这些答案都没错,但是我好像还没有找到我内心的答案。


灵感


休假结束后,我继续回到公司“搬砖”,日子就这样一天天过去,直到某一天周末,那天天气很好,临时起意想去附近的海珠湖看看,走在湖边的林荫道,听着鸟语,闻着花香,望着波光粼粼的湖面和湖面上惬意游动的野鸭群,微风夹杂着泥土的气息拂面而过,我感觉到心旷神怡,身心放松,最后在湖边一直待到了日落才回家。


在回家的路上,我感觉这一天美好而充实,有一种久违的“活着”的感觉,这样的一天应该被记录下来,于是我发了上一篇图文:2022.08.20,而且我突然有了灵感,今天的湖边漫步也可以看做一次短暂的旅行,从中我好像找到了我想要的答案:旅行的意义,就在于给生命留下更多的回忆。


那么更多的回忆有什么用呢?


主观时间与客观时间


客观的时间是匀速而精确的,一秒一分一天一年,因为它就是人类发明出来用于衡量的一种工具;但是主观的时间却不是,相信很多人都有这样一种感觉,随着年龄的增长感觉时间过得越来越快


小时候感觉时间过得很慢,度日如年,盼望着墙上的钟转快点,盼望着能早点下课,盼望着能快点长大;而长大后(尤其是参加工作后),却感觉时间过得很快,度年如日,仿佛一眨眼,青春就不在了。(想想你过去几年记得的日子有哪些?)


这就是「主观时间的加速」,为什么会有这个现象呢,主要是因为单位时间内留下的回忆密度不同,小时候经历的事物少,每天都是崭新的一天,新的知识、新的朋友、新的世界,我们的大脑会对这些新鲜的事物产生印象,形成许多回忆;而长大后,除非主动探索,否则其实很少再遇到新鲜的事物,每天走相同的路,接触相同的人,我们的大脑不会记忆这些重复的事物,也就不会留下记忆,久而久之就会感觉过去一周,一个月甚至一年也没留下什么有印象的事


一个人生命的长度当然可以用客观存活时间来衡量,但是在了解了主观时间的加速之后,就会发现用客观时间衡量其实并不准确,假如一个人活了100岁,但是其生命中的大部分日子都是重复的,没有回忆的,那其实他的主观生命长度可能并没有一个只活了50岁却拥有丰富人生回忆的人长,所以在追求客观时间上长寿的同时,我们更应该追求的是主观时间上的“长寿”


所以回到前面的问题,回忆有什么用,答案就是:回忆可以增加一个人主观生命的长度


旅游就是一中很好的留下回忆的方式,来到一个新的地方,会遇见很多你没见过、听过、尝过的事物,这会充分调动我们的每一种感官,将这些新鲜事物记录在我们大脑中,形成回忆。想到这里,我似乎能够理解那些变卖家产环游世界的人了。


新的体验


当然,不是所有人都想要或者能够环游世界,旅行也算是个有一定门槛的活动,需要时间和金钱的支撑。那么除了旅行之外,有没有别的方式来增加我们的回忆,从而延长主观生命呢?


很多人把旅行定义为 “看不同的风景,遇见不同的人”,其中关键不在风景和人,而在于不同二字,因为有区别于之前不同的体验,才能引起大脑的注意并留下印象,所以只要是能带来「新的体验」的事物都可以增加我们的回忆。(还有一点可以增加回忆,就是带来较大情绪的事物,比如某次成功或者失败,也很容易理解,这里就不展开了)


新的体验可以来自很多地方,也可大可小,比如尝试一项新的运动,吃一家新的餐厅,学一门新的乐器,甚至走一条不同的路,当然也不仅局限于生活中,工作上我们也可以积极去寻找新的体验,不断拓宽自己的边界


再说回旅行的事,旅行确实可以留下回忆,但是这其实是旅行的结果,而在旅行的过程中,我们应该学会感受当下,就像那次“湖边漫步”和前面说的各种答案,尽情地去欣赏美景、品尝美食、了解世界、发朋友圈、交友,美好的回忆自然就会产生。


最后


马上就到国庆了,我准备计划一场旅行,还是像往常一样地“逛吃逛吃”,但是这次,我想我的内心会变得更加清澈和坚定。


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

Htmx 意外走红,我们从 React“退回去”后:代码行数减少 67%,JS 依赖项从 255 下降到 9

htmx 的走红 过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却...
继续阅读 »

htmx 的走红


过去 Web 非常简单。URL 指向服务器,服务器将数据混合成 html,然后在浏览器上呈现该响应。围绕这种简单范式,诞生了各种 Javascript 框架,以前可能需要数月时间完成的一个应用程序基本功能,现在借助这些框架创建相对复杂的项目却只需要数小时,我们节省了很多时间,从而可以将更多精力花在业务逻辑和应用程序设计上。


但随着 Web 不断地发展,Javascript 失控了。不知何故,我们决定向用户抛出大量 App,并在使用时发出不断增加的网络请求;不知何故,为了生成 html,我们必须使用 JSON,发出数十个网络请求,丢弃我们在这些请求中获得的大部分数据,用一个越来越不透明的 JavaScript 框架黑匣子将 JSON 转换为 html,然后将新的 html 修补到 DOM 中......


难道大家快忘记了我们可以在服务器上渲染 html 吗?更快、更一致、更接近应用程序的实际状态,并且不会向用户设备发送任何不必要的数据?但是如果没有 Javascript,我们必须在每次操作时重新加载页面。



现在,有一个新的库出现了,摒弃了定制化的方法,这就是 htmx。作为 Web 开发未来理念的一种实现,它的原理很简单:

  • 从任何用户事件发出 AJAX 请求。

  • 让服务器生成代表该请求的新应用程序状态的 html。

  • 在响应中发送该 html。

  • 将该元素推到它应该去的 DOM 中。


htmx 出现在 2020 年,创建者Carson Gross 说 htmx 来源自他于 2013 年研究的一个项目intercooler.js。2020 年,他重写了不依赖 jQuery 的 intercooler.js,并将其重命名为 htmx。然后他惊讶的发现 Django 社区迅速并戏剧性地接受了它!



图片来源:lp.jetbrains.com/django-deve…2021-486/


Carson Gross认为 htmx 设法抓住了开发者对现有 Javascript 框架不满的浪潮,“这些框架非常复杂,并且经常将 Django 变成一个愚蠢的 JSON 生产者”,而 htmx 与开箱即用的 Django 配合得更好,因为它通过 html 与服务器交互,而 Django 非常擅长生成 html。


对于 htmx 的迅速走红,Carson Gross 发出了一声感叹:这真是“十年窗下无人问,一举成名天下知(this is another example of a decade-long overnight success)”。


htmx 的实际效果


可以肯定的一点是 htmx 绝对能用,单从理论上讲,这个方法确实值得称道。但软件问题终究要归结于实践效果:效果好吗,能不能给前端开发带来改善?


在 DjangoCon 2022 上,Contexte 的 David Guillot 演示了他们在真实 SaaS 产品上实现了从 React 到 htmx 的迁移,而且效果非常好,堪称“一切 htmx 演示之母”(视频地址:http://www.youtube.com/watch?v=3GO…)。


Contexte 的项目开始于 2017 年,其后端相当复杂,前端 UI 也非常丰富,但团队非常小。所以他们在一开始的时候跟随潮流选择了 React 来“构建API绑定 SPA、实现客户端状态管理、前后端状态分离”等。但实际应用中,因为 API 设计不当,DOM 树太深,又需要加载很多信息,导致 UI“非常非常缓慢”。在敏捷开发的要求下,团队里唯一的 Javascript 专家对项目的复杂性表现得一无所措,因此他们决定试试 htmx。


九大数据提升



于是我们决定大胆尝试,花几个月时间用简单的 Django 模板和 htmx 替换掉了 SaaS 产品中已经使用两年的 React UI。这里我们分享了一些相关经验,公布各项具体指标,希望能帮同样关注 htmx 的朋友们找到说服 CTO 的理由!

  • 这项工作共耗费了约 2 个月时间(使用 21K 行代码库,主要是 JavaScript)

  • 不会降低应用程序的用户体验(UX)

  • 将代码库体积减小了 67%(由 21500 行削减至 7200 行)

  • 将 Python 代码量增加了 140%(由 500 行增加至 1200 行);这对更喜欢 Python 的开发者们应该是好事

  • 将 JS 总体依赖项减少了 96%(由 255 个减少至 9 个)


  • 将 Web 构建时间缩短了 88%(由 40 秒缩短至 5 秒)

  • 首次加载交互时间缩短了 50%至 60%(由 2 到 6 秒,缩短至 1 到 2 秒)

  • 使用 htmx 时可以配合更大的数据集,超越 React 的处理极限

  • Web 应用程序的内存使用量减少了 46%(由 75 MB 降低至 40 MB)



这些数字令人颇为意外,也反映出 Contexte 应用程序高度契合超媒体的这一客观结果:这是一款以内容为中心的应用程序,用于显示大量文本和图像。很明显,其他 Web 应用程序在迁移之后恐怕很难有同样夸张的提升幅度。


但一些开发者仍然相信,大部分应用程序在采用超媒体/htmx 方法之后,肯定也迎来显著的改善,至少在部分系统中大受裨益。


开发团队组成


可能很多朋友没有注意,移植本身对团队结构也有直接影响。在 Contexte 使用 React 的时候,后端与前端之间存在硬性割裂,其中两位开发者全职管理后端,一位开发者单纯管理前端,另有一名开发者负责“全栈”。(这里的「全栈」,代表这位开发者能够轻松接手前端和后端工作,因此能够在整个「栈」上独立开发功能。)



而在移植至 htmx 之后,整个团队全都成了“全栈”开发人员。于是每位团队成员都更高效,能够贡献出更多价值。这也让开发变得更有乐趣,因为开发人员自己就能掌握完整功能。最后,转向 htmx 也让软件优化度上了一个台阶,现在开发人员可以在栈内的任意位置进行优化,无需与其他开发者提前协调。


htmx 是传统思路的回归


如今,单页应用(SPA)可谓风靡一时:配合 React、Redux 或 Angular 等库的 JS 或 TS 密集型前端,已经成为创建 Web 应用程序的主流方式。以一个需要转译成 JS 的 SPA 应用为例:



但 htmx 风潮已经袭来,人们开始强调一种“傻瓜客户端”方法,即由服务器生成 html 本体并发送至客户端,意味着 UI 事件会被发送至服务器进行处理。



用这个例子进行前后对比,我们就会看到前者涉及的活动部件更多。从客户端角度出发,后者其实回避了定制化客户端技术,采取更简单的方法将原本只作为数据引擎的服务器变成了视图引擎。


后一种方法被称为 AJAX(异步 JavaScript 与 XML)。这种简单思路能够让 Web 应用程序获得更高的响应性体验,同时消除了糟糕的“回发”(postback,即网页完全刷新),由此回避了极其低效的“viewstate”等.NET 技术。


htmx 在很多方面都体现出对 AJAX 思路的回归,最大的区别就是它仅仅作为新的声明性 html 属性出现,负责指示触发条件是什么、要发布到哪个端点等。


另一个得到简化的元素是物理应用程序的结构与构建管道。因为不再涉及手工编写 JS,而且整个应用程序都基于服务器,因此不再对 JS 压缩器、捆绑器和转译器做(即时)要求。就连客户端项目也能解放出来,一切都由 Web 服务器项目负责完成,所有应用程序代码都在.NET 之上运行。从这个角度来看,这与高度依赖服务器的Blazor Server编程模型倒是颇有异曲同工之妙。


技术和软件开发领域存在一种有趣的现象,就是同样的模式迭起兴衰、周而复始。随着 SPA 的兴起,人们一度以为 AJAX 已经过气了,但其基本思路如今正卷土重来。这其中当然会有不同的权衡,例如更高的服务器负载和网络流量(毕竟现在我们发送的是数据视图,而不只是数据),但能让开发者多个选择肯定不是坏事。


虽然不敢确定这种趋势是否适用于包含丰富用户体验的高复杂度应用程序,但毫无疑问,相当一部分 Web 应用程序并不需要完整的 SPA 结构。对于这类用例,简单的 htmx 应用程序可能就是最好的解决方案。


参考链接:


news.ycombinator.com/item?id=332…


htmx.org/essays/a-re…


http://www.reddit.com/r/django/co…


mekhami.github.io/2021/03/26/…


http://www.compositional-it.com/news-blog/m…


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

揭秘 Google Cloud Next '23:生成式 AI 的探索之路与开发范式变革

戳这里了解更多 前言: 8 月底,谷歌以「AI 与云科技驱动创新」为题,举办了为期三天的 Google Cloud Next ’23 大会,展示了谷歌在基础架构、数据和 AI、Workspace 协作和信息安全解决方案等全系列产品不断创新的成果。 乍看之下似乎...
继续阅读 »


戳这里了解更多


前言:


8 月底,谷歌以「AI 与云科技驱动创新」为题,举办了为期三天的 Google Cloud Next ’23 大会,展示了谷歌在基础架构、数据和 AI、Workspace 协作和信息安全解决方案等全系列产品不断创新的成果。


乍看之下似乎又是一场「大而全」的行业大会,但全程看完之后会明显的感受到,本次大会的内容全部围绕住了一个重心 —— 「生成式 AI」。


生成式 AI 作为近一两年最热门的技术话题没有之一,大家的谈论早已经超出了技术的范畴。如何应用、如何融合、如何落地,各行各业都在探索生成式 AI 带来的可能性。但除了 ChatGPT 这类的聊天机器人,似乎还没有特别成功的落地工具或者应用,哪怕是技术本源所在的研发领域也如是。


谷歌这次,似乎给出了一个参考答案。


一、Google Next '23:生成式 AI 的探索之路


生成式 AI 与传统 AI 技术最根本的区别在于前者通过理解自然语言创建内容,而后者依赖的是编程语言,这是生成式 AI 技术的关键变革特征,也是以前从未有过的能力。并且生成式 AI 能够以文本、图像、视频、音频和代码的形式生成新内容,而传统的 AI 系统训练计算机对人类行为、商业结果等进行预测。


对于许多人来说,第一次切身感知到生成式 AI 技术就是通过 ChatGPT。作为一种人工智能聊天机器人,在 2022 年 11 月迅速风靡全球。


大部分人不知道的是,ChatGPT 在架构层使用的是 Transformer 这一语言处理架构,该架构实际上便是谷歌在 2017 年的论文《Attention Is All You Need》中提出的。


谷歌作为一家成立了 25 年的公司,曾经在搜索、邮箱等领域取得了很多成绩,但在 AI 领域却面临了一些质疑。此前有媒体表示“谷歌在人工智能领域没有‘秘密武器’,无法赢得这场竞争。”而今年 5 月份的 Google I/O 以及前几日的 Google Cloud Next '23,可能正是在某种程度上回击了这种言论。


正如 Alphabet 和谷歌首席执行官桑达尔·皮查伊 (Sundar Pichai) 在活动开幕式上表示:


在过去几年与企业领导者的交谈中,我听到了一个类似的主题。从桌面到移动,到云,再到现在的人工智能,他们需要的是一直走在技术突破前沿的合作伙伴。很多转变确实令人兴奋,但同时也会带来不确定性。向人工智能的转变无疑就是如此。


作为一家公司,我们已经为这一时刻准备了一段时间。在过去的七年里,我们采取了人工智能先行的方法,应用人工智能使我们的产品从根本上更加可用。我们相信,让人工智能为每个人带来帮助,是我们在未来十年完成使命的最重要方式。


先内部小规模测试,再面向大众开放成熟的能力。谷歌也许确实没有“秘密武器”,但可能重点在于并不需要“秘密”,准备好之后,拿出来大家正面比划一下。这次的大会中,谷歌便亮出了其武器:


1**、**Cloud TPU v5e


生成式 AI 带来许多先进的功能,并可广泛使用于各种应用,但不可否认的是更加迫切的需要更先进、更强大的基础架构,设计和构建计算基础设施的传统方法已不足以满足生成式 AI 和大语言模型 (LLM) 等新兴工作负载的需求。为了解决这个问题,谷歌推出了 Cloud TPU v5e,一款最新且最具成本效益的 TPU。


TPU 是专门为大型人工智能模型的训练和推理而设计的定制人工智能芯片。客户可以使用单个 Cloud TPU 平台来运作大规模 AI 训练和推理。根据大会公开信息展示,Cloud TPU v5e 可扩展到数万个芯片并针对效率进行了优化。与 Cloud TPU v4 相比,每美元的训练效率可提升 2 倍,每美元的推论效率可提升 2.5 倍。


2**、**Vertex AI


在 2021 年 Google I/O 大会中,谷歌推出了 Vertex AI 托管式机器学习平台,用来帮助开发者更轻松地构建、部署和维护其机器学习模型。在本次的大会上,则正式推出了 Vertex AI 的搜索和对话功能,并将 ML 模型数量增加到 100 多个,这些模型都依据不同任务和不同大小进行了优化,包括文本、聊天、图像、语音、软件代码等等。


为了进一步平衡用户使用大模型进行建模的灵活性,以及他们可以生成的场景与推理成本以及微调能力,谷歌还为 Vertex AI 带来了扩展功能和 Grounding 等新的功能和工具。


借助 Vertex AI 扩展功能,开发者可以将 Model Garden 模型库中的模型与实时数据、专有数据或第三方平台(如 CRM 系统或电子邮件)连接起来,从而提供即时信息、集成公司数据并代表用户采取行动。这为生成式 AI 应用程序开辟了无限的新可能性。


Grounding 则是适用于 Vertex AI 基础模型、搜索及对话(Search and Conversation)的一项服务,可以协助客户将回复纳入企业自身的数据中,以提供更准确的回复内容。这一功能的重点在于可以一定程度上避免现阶段 AI 的“胡言乱语”,从而规避一些风险或者问题。


3**、**Duet AI


在 5 月的 I/O 大会上,Google Cloud 推出了 Duet AI。官方将其描述为“一位重要的协作伙伴、教练、灵感来源,和生产力推进器”,比如将 Docs 大纲转换成 Slides 中的演示文档,根据表格中的数据生成对应的图表;或者把 Duet AI 当做一个创作型的工具,用它来撰写电子邮件、生成图像、做会议纪要、检查文章的语法错误等等。


但当时的 Duet AI 只能在 Workspace 中使用,这次则扩展到了 Google Cloud 和 BigQuery 中,并推出更多适用的 AI 功能。例如 BigQuery 中的 Duet AI 旨在通过生成完整的函数和代码块,让用户专注于逻辑结果。它还可以建议和编写 Python 代码和 SQL 查询。这将进一步发挥 Duet AI "编码专家、软件可靠性工程师、数据库专家、数据分析专家和网络安全顾问 "的作用。


数据是生成式 AI 的核心,不难看出谷歌这次的更新迭代正式为了帮助数据团队进一步提高生产力,协助组织发挥数据及 AI 的最大潜力。


二、一些后续思考:生成式 AI 带来的开发范式变革


从基建、到平台再到应用,草蛇灰线,伏脉千里。谷歌在生成式 AI 领域的探索,其实并不像大家所想的有些“掉队”,而是在另一个维度提前布局。


25 年来,谷歌不断投资数据中心和网络,现在已经拥有涵盖 38 个云区域的全球网络,根据官方所说,目标是在 2030 年完全实现全天候采用无碳能源维持运营。谷歌的 AI 基础架构也在业界占据很大的份额,有超过 70% 的生成式 AI 独角兽公司和超过一半获得融资的生成式 AI 初创公司,都是 Google Cloud 客户。


"我们从每一层开始。这是对整个堆栈的重新构想。"这是英伟达的黄仁勋在 Google Cloud Next '23 中传递的一个态度,"生成式人工智能正在彻底改变计算堆栈的每一层。我们两家公司(英伟达和谷歌)拥有世界上最有才华的两支计算科学团队,将为生成式人工智能重新发明云基础设施。"


开发者关注的,是如何借助生成式 AI 的能力&工具提效;企业关注的,是如何借助生成式 AI 来迭代业务产品抢占市场心智。但对谷歌这类“搞基建”的公司而言,关注堆栈的每一层、关注堆栈的整体结构,才有可能推进技术的发展,实现传统开发范式的变革。


今年年初,谷歌推出了 Security AI Workbench,这是业界首创的可扩展平台,由谷歌的新一代安全性大语言模型 Sec-PaLM 2 驱动,结合了谷歌独有的观测技术,能帮助开发者掌握不断变化的安全性威胁,并针对网络安全操作进行微调。


几周前,谷歌推出 Chronicle CyberShield,能解决数据孤岛的问题,也能集中管理安全性数据,并统一规划处理方式。


“我们正处于一个由人工智能推动的全新数字化转型时代,”Google Cloud 首席执行官库里安说,“这项技术已经在改善企业的运营方式以及人类之间的互动方式。它正在改变医生照顾病人的方式、人们沟通的方式,甚至我们在工作中的安全方式。而这仅仅是个开始。”


生成式 AI 通过 ChatGPT 类的工具产品,已经在艺术创作、代码生成等领域带来了未曾设想过的便利,随着基础设施的迭代演进,相信现阶段的开发范式变革,可能真的仅仅是个开始。


结语:


生成式 AI 兴起之后,业界纷纷提出“想象力等于生产力”之类的观点,并借助一些场景的应用为佐证。谷歌这次的大会发布,无论是对生成式 AI 技术的推动,还是开发工具&服务的迭代,都给了我们更多的信心与想象的方向。


无论是从 AI 最佳化的基础架构,到注入了生成式 AI 强大功能的数据分析和信息安全服务;还是从增加了更多新模型和工具的 Vertex AI 平台,到扩大了支持 Duet AI 的 Workspace 和 Google Cloud,这些技术都是难得的探索与尝试,这些演变或者变革都是迈向下一次重大演变的正确方向的垫脚石。


对于开发者这一最了解技术本质的人群而言,我们能做的就是拥抱变化与发展,与企业、社区、生态一起,持续探索与创新。在变革到来前,找到要去的方向;在变革到来后,找到自己的位置。


Tips:会后的配套学习资料,给你准备好了!


为了让中国的开发者们更好地 Get 新技术、新发展,Google Cloud 今年同样安排了会后的配套系列课程 —— 「Next ’23 中文精选课」。


今年的课程将聚焦 AI/ML、安全合规、数据库、数据分析、DevOps、应用程序开发等领域,解读最新技术发布与行业实践应用,解读 Next ’23 发布的 100 项创新成果 。


发布会中没来得及讲的、没讲完的,都在这次的课程中了👌


据官方的信息展示,这次的中文精选课不仅有技术干货,更给开发者提供了多种互动方式体验,以及一批 Google Cloud 官方周边礼品,旅行颈枕、无线鼠标、电竞游戏耳机、蓝牙音箱,甚至还有 Google 25 周年纪念版安卓小人!


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

iOS Universal link

iOS
1. Universal link 介绍 1.1 Universal link 是什么 Universal Link 是苹果在 WWDC 上提出的 iOS9 的新特性之一。此特性类似于深层链接,并能够方便地通过打开一个 Https 链接来直接启动您的客户端应用...
继续阅读 »

1. Universal link 介绍


1.1 Universal link 是什么


Universal Link 是苹果在 WWDC 上提出的 iOS9 的新特性之一。此特性类似于深层链接,并能够方便地通过打开一个 Https 链接来直接启动您的客户端应用(手机有安装 App)。对比起以往所使用的 URL Scheme,这种新特性在实现 web-app 的无缝链接时能够提供极佳的用户体验。


当你的应用支持 Universal Link(通用链接),当用户点击一个链接是可以跳转到你的网站并获得无缝重定向到对应的 APP,且不需要通过 Safari 浏览器。如果你的应用不支持的话,则会在 Safari 中打开该链接。在苹果开发者中可以看到对它的介绍是:



Seamlessly link to content inside your app, or on your website in iOS 9 or later. With universal links, you can always give users the most integrated mobile experience, even when your app isn’t installed on their device.



1.2 Universal link 的应用场景


使用 Universal Link(通用链接)可以让用户在 Safari 浏览器或者其他 APP 的 webview 中拉起相应的 APP,也可以在 APP 中使用相应的功能,从而来把用户引流到 APP 中。


这具体是一种怎样的情景呢?举个例子,你的用户 safari 里面浏览一个你们公司的网页,而此时用户手机也同时安装有你们公司的 App;而 Universal Link 能够使得用户在打开某个详情页时直接打开你的 app 并到达 app 中相应的内容页面,从而实施用户想要的操作(例如查看某条新闻,查看某个商品的明细等等)。比如在 Safari 浏览器中进入淘宝网页点击打开 APP 则会使用 Universal Link(通用链接)来拉起淘宝 APP。


1.3 Universal link 跳转的好处

  • 唯一性: 不像自定义的 URL Scheme,因为它使用标准的 HTTPS 协议链接到你的 web 站点,所以一般不会被其它的 APP 所声明。另外,URL scheme 因为是自定义的协议,所以在没有安装 app 的情况下是无法直接打开的(在 Safari 中还会出现一个不可打开的弹窗),而 Universal Link(通用链接)本身是一个 HTTPS 链接,所以有更好的兼容性;

  • 安全: 当用户的手机上安装了你的 APP,那么系统会去你配置的网站上去下载你上传上去的说明文件(这个说明文件声明了当前该 HTTPS 链接可以打开那些 APP)。因为只有你自己才能上传文件到你网站的根目录,所以你的网站和你的 APP 之间的关联是安全的;

  • 可变: 当用户手机上没有安装你的 APP 的时候,Universal Link(通用链接)也能够工作。如果你愿意,在没有安装你的 app 的时候,用户点击链接,会在 safari 中展示你网站的内容;

  • 简单: 一个 HTTPS 的链接,可以同时作用于网站和 APP;

  • 私有: 其它 APP 可以在不需要知道你的 APP 是否安装了的情况下和你的 APP 相互通信。


2. Universal link 配置和运行


2.1 配置 App ID 支持 Associated Domains


登录developer.apple.com/ 苹果开发者中心,找到对应的 App ID,在 Application Services 列表里有 Associated Domains 一条,把它变为 Enabled 就可以了。



2.2 配置 iOS App 工程


Xcode 11.0 版本


工程配置中相应功能:targets->Signing&Capabilites->Capability->Associated Domains,在其中的 Domains 中填入你想支持的域名,也必须必须以 applinks:为前缀。


具体步骤如下图:





Xcode 11.0 以下版本


工程配置中相应功能:targets->Capabilites->Associated Domains,在其中的 Domains 中填入你想支持的域名,必须以 applinks:为前缀。


配置项目中的 Associated Domains:



2.2 配置和上传 apple-app-association


究竟哪些的 url 会被识别为 Universal Link,全看这个 apple-app-association 文件Apple Document UniversalLinks.html

  • 你的域名必须支持 Https

  • 域名 根目录 或者 .well-known 目录下放这个文件apple-app-association,不带任何后缀

  • 文件为 json 保存为文本即可

  • json 按着官网要求填写即可


apple-app-site-association模板:

{    "applinks": {        "apps": [],        "details": [            {                "appID": "9JA89QQLNQ.com.apple.wwdc",                "paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]            },            {                "appID": "ABCD1234.com.apple.wwdc",                "paths": [ "*" ]            }        ]    }}

复制代码


说明:



appID: 组成方式是 teamId.yourapp’s bundle identifier。如上面的 9JA89QQLNQ 就是 teamId。登陆开发者中心,在 Account -> Membership 里面可以找到 Team ID。




paths: 设定你的 app 支持的路径列表,只有这些指定的路径的链接,才能被 app 所处理。星号的写法代表了可识 别域名下所有链接。



上传指定文件:上传该文件到你的域名所对应的根目录或者.well-known 目录下,这是为了苹果能获取到你上传的文件。上传完后,自己先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载 apple-app-site-association 文件。


2.4 如何验证 Universal link 生效

  • 可以使用 iOS 自带的备忘录程序,输入链接,长按链接,如果弹出菜单中有”在‘xxx’中打开”,即表示配置生效。

  • 或者将要测试的网址在Safari中打开,在出现的网页上方下滑,可以看到有在”xxx”应用中打开, 出现菜单:



当点击某个链接,直接可以进我们的 app 了,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容。


AppDelegate里中实现代理方法,官方链接:Handling Universal Links


Objective-C:

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {    if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb])    {        NSURL *url = userActivity.webpageURL;        if (url是我们希望处理的)        {            //进行我们的处理        }        else        {            [[UIApplication sharedApplication] openURL:url];        }    }         return YES;}

复制代码


Swift:

func application(_ application: UIApplication,                 continue userActivity: NSUserActivity,                 restorationHandler: @escaping ([Any]?) -> Void) -> Bool{    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,        let incomingURL = userActivity.webpageURL,        let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),        let path = components.path,        let params = components.queryItems else {            return false    }        print("path = (path)")        if let albumName = params.first(where: { $0.name == "albumname" } )?.value,        let photoIndex = params.first(where: { $0.name == "index" })?.value {                print("album = (albumName)")        print("photoIndex = (photoIndex)")        return true            } else {        print("Either album name or photo index missing")        return false    }}

复制代码


3. Universal link 遇到的问题和解决方法


3.1 跨域


前端开发经常面临跨域问题,恩 Universal Link 也有跨域问题,但不一样的是,Universal Link,必须要求跨域,如果不跨域,就不行,就失效,就不工作。(iOS 9.2 之后的改动,苹果就这么规定这么设计的)


这也是上面拿知乎举例子的时候重点强调的一个问题,知乎为什么使用oia.zhihu.com做 Universal Link?

  • 假如当前网页的域名是 A

  • 当前网页发起跳转的域名是 B

  • 必须要求 B 和 A 是不同域名,才会触发 Universal Link

  • 如果 B 和 A 是相同域名,只会继续在当前 WebView 里面进行跳转,哪怕你的 Universal Link 一切正常,根本不会打开 App


是不是不太好理解,那直接拿知乎举例子


有心人可能看到,知乎的 Universal Link 配置的是 oia.zhihu.com 这个域名,并且对这个域名下比如/answers /questions /people 等 urlpath 进行了识别,也就是说,知乎的 universal link,只有当你访问 https://oia.zhihu.com/questions/xxxx,在移动端会触发 Universal Link,而知乎正经的 Urlhttps//www.zhihu.com/questions/xxx是不会触发 Universal Link 的,知乎为什么制作,为什么不把他的主域名配置 Universal Link,就是由于 Universal Link 的跨域的原因。


知乎的一般网页 URL 都是http://www.zhihu.com域名,你在微信朋友圈看到了知乎的问题分享,如果 copy url 你就能看到这样的链接


http://www.zhihu.com/question/22…



微信里其实是屏蔽 Schema 的,但是你依然能看到大大的一个按钮App内打开,这确实就是通过 Universal Link 来实现的,但如果知乎把 Universal Link 配在了http://www.zhihu.com域名,那么即便已经安装了 App,Universal Link 也是不会生效的。


一般的公司都会有自己的主域名,比如知乎的http://www.zhihu.com,在各处分享传播的时候,也都是直接分享基于主域名的 url,但为了解决苹果强制要求跨域才生效的问题,Universal Link 就不能配置在主域名下,于是知乎才会准备一个oia.zhihu.com域名,专为 Universal Link 使用,不会跟任何主动传播分享的域名撞车,从而在任何活动 WAP 页面里,都能顺利让 Universal Link 生效。


跨域的另外一个好处是可以突破微信跳转限制,支持微信无缝跳转到 App.


简单一句话



只有当前 webview 的 url 域名,与跳转目标 url 域名不一致时,Universal Link 才生效



3.2 更新


apple-app-association 的更新时机有以下两种:

  • 每次 App 安装后的第一次 Launch,会拉取 apple-app-association

  • Appstore 每次 App 的版本更新后的第一次 Launch,也会更新 apple-app-association


所以反复重新杀 APP 重开完全没用,删了 App 重装确实有用,但不可能让用户这么去做。也就是说,一旦不小心因为意外 apple-app-association,想要挽回又让那部分用户无感,App 再发一个版本就好了


3.3 Universal Link 用户行为


Universal Link 触发后打开 App,这时候 App 的状态栏右上角会有文字提示来自 XXApp,可以点状态栏的文字快速返回原来的 AP


如果用户点了返回微信,就会被苹果记住,认为用户并不需要跳出原 App 打开新 App,因此这个 App 的 Universal Link 会被关闭,再也无效。


想要开启也不是不行,让用户重新用 safari 打开,universal link 的页面,然后会出现很像苹果 smart bar 的东西,那个东西点了后就能打开


4. H5 端的 Universal Link 业务部署


H5 端的 Universal Link 跳转,从产品经理的角度看,需要满足以下 2 个需求:

  • 如果已安装 App,跳转对应界面

  • 如果没安装 App,跳转 App 下载界面


H5 端部署 Universal Link 示例:

router.use('/view', function (req, res, next) {    var path = req.path;    res.redirect('https://www.xxx.com/view' + path + '?xxx=xxx');});

复制代码


整个效果就是

  • 跳转https://www.xxx.com/view/*

  • 已安装 App

  • 打开 App 触发 handleUniversalLink

  • 走到/view/分支,拼接阅读页路由跳转

  • 未安装 AppWebView

  • 原地跳转https://``www.xxx.com``/view/*

  • 命中服务器的重定向逻辑

  • 重定向到https://``www.xxx.com``/view/*

  • 打开相应的 H5 页面



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

价格.0处理

iOS
在项目中有500.0或者500.00的情况需要处理 实习的同学写了一段这样的代码public extension String { var trimZero: String { replacingOccurrences(of: ".00...
继续阅读 »

在项目中有500.0或者500.00的情况需要处理


实习的同学写了一段这样的代码

public extension String {
var trimZero: String {
replacingOccurrences(of: ".00", with: "").replacingOccurrences(of: ".0", with:"")
}
}

咋一看似乎没啥问题,结果也符合预期




但是上面的case其实没有覆盖全,例如:500.01,那上面的处理方式就有bug了,会被处理成5001


正确的处理方式

public extension String {
var trimZero: String {
guard let value = Double(self) else { return self }
let formatter = NumberFormatter()
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 2
return formatter.string(from: NSNumber(value: value)) ?? self
}
}

测试结果




参考



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