请点赞关注,你的支持对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

大家好,我是小彭。2 年前,我们在 为了组件化改造学习十几家大厂的技术博客 这篇文章里收集过各大厂的组件化方案。其中,有美团收银团队分享的组件化总线框架 modular-event 让我们印象深刻。然而,美团并未将该框架开源,我们只能望梅止渴。

在学习和借鉴美团 modular-event 方案中很多优秀的设计思想后,我亦发现方案中依然存在不一致风险和不足,故我决定对方案进行改进并向社区开源。项目主页为 Github · ModularEventBus,演示 Demo 可直接下载:

Demo apk

欢迎提 Issue 帮助修复缺陷,欢迎提 Pull Request 增加新的 Feature,有用请点赞给 Star,给小彭一点创作的动力,谢谢。


这篇文章是 组件化系列文章第 5 篇,相关 Android 工程化专栏完整文章列表:

一、Gradle 基础:

二、AGP 插件:

三、组件化开发:

四、AOP 面向切面编程:

五、相关计算机基础


1. 认识事件总线

1.1 事件总线的优点

事件总线框架最大的优点是 ”解耦“,即事件发布者与事件订阅者的解耦,事件的发布者不需要关心是否有人订阅该事件,也不需要关心是谁订阅该事件,代码耦合度较低。因此,事件总线框架更适合作为全局的事件通信方案,或者组件间通信的辅助方案。

1.2 事件总线的缺点

然而,成也萧何败萧何。有人觉得事件总线好用,亦有人觉得事件总线不好用,归根结底还是因为事件总线太容易被滥用了,用时一时爽,维护火葬场。我将事件总线框架存在的问题概括为以下 5 种常见问题:

  • 1、消息难溯源: 在阅读源码的过程中,如果需要查找发布事件或订阅事件的地方,只能通过查找事件引用的方式进行溯源,增大了理解代码逻辑的难度。特别是当项目中到处是临时事件时,难度会大大增加;

  • 2、临时事件滥用: 由于框架对事件定义没有强制约束,开发者可以随意地在项目的各个角落定义事件。导致整个项目都是临时事件飞来飞去,增大后期维护的难度;

  • 3、数据类型转换错误: LiveDataBus 等事件总线框架需要开发者手动输入事件数据类型,当订阅方与发送方使用不同的数据类型时,会发生类型转换错误。在发生事件命名冲突时,出错的概率会大大增加,存在隐患;

  • 4、事件命名重复: 由于框架对事件命名没有强制约束,不同组件有可能定义重名的事件,产生逻辑错误。如果重名的事件还使用了不同的数据类型,还会出现类型转换错误,存在隐患;

  • 5、事件命名疏忽: 与 ”事件命名重复“ 类似,由于框架对事件命名没有检查,有可能出现开发者复制粘贴后忘记修改事件变量值的问题,或者变量值拼写错误(例如 login_success 拼写为 login_succese),那么订阅方将永远收不到事件。

1.3 ModularEventBus 的解决方案

ModularEventBus 组件化事件总线框架的优点是: 在保持发布者与订阅者的解耦的优势下,解决上述事件总线框架中存在的通病。 具体通过以下 5 个手段实现:

  • 1、事件声明聚合: 发布者和订阅者只能使用预定义的事件,严格禁止使用临时事件,事件需要按照约定聚合定义在一个文件中(解决临时事件滥用问题);

  • 2、区分不同组件的同名事件: 在定义事件时需要指定事件所属 moduleName,框架自动使用 "[moduleName]$$[eventName]" 作为最终的事件名(解决事件命名重复问题);

  • 3、事件数据类型声明: 在定义事件时需要指定事件的数据类型,框架自动使用该数据类型发送和订阅事件(解决数据类型转换错误问题);

  • 4、接口强约束: 运行时使用事件类发布和订阅事件,框架自动使用事件定义的事件名和数据类型,而不需要手动输入事件名和数据类型(解决事件命名命名错误);

  • 5、APT 生成接口类: 框架在编译时使用 APT 注解处理器自动生成事件接口类。

1.4 与美团 modular-event 对比有哪些什么不同?

  • modular-event 使用静态常量定义事件,为什么 ModularEventBus 用接口定义事件?

    美团 modular-event 使用常量引入了重复信息,存在不一致风险。例如开发者复制一行常量后,只修改常量名但忘记修改值,这种错误往往很难被发现。而 ModularEventBus 使用方法名作为事件名,方法返回值作为事件数据类型,不会引入重复信息且更加简洁。

modular-event 事件定义

  • modular-event 使用动态代理,为什么 ModularEventBus 不需要?

    美团 modular-event 使用动态代理 API 统一接管了事件的发布和订阅,但考虑到这部分代理逻辑非常简单(获取事件名并交给 LiveDataBus 完成后续的发布和订阅逻辑),且框架本身已经引入了编译时 APT 技术,完全可以在编译时生成这部分代理逻辑,没必要使用动态代理 API。

  • 更多特性支持:

    此外 ModularEventBus 还支持生成事件文档、空数据拦截、泛型事件、自动清除空闲事件等特性。


2. ModularEventBus 能做什么?

ModularEventBus 是一款帮助 Android App 解决事件总线滥用问题的框架,亦可作为组件化基础设施。 其解决方案是通过注解定义事件,由编译时 APT 注解处理器进行合法性检查和自动生成事件接口,以实现对事件定义、发布和订阅的强约束。

2.1 常见事件总线框架对比

以下从多个维度对比常见的事件总线框架( 良好支持、️ 支持、 不支持):

事件总线 ModularEventBus modular-event SmartEventBus LiveEventBus LiveDataBus EventBus RxBus
开发者 @彭旭锐 @美团 @JeremyLiao @JeremyLiao / @greenrobot /
Github Star 0 未开源 146 3.4k / 24.1k /
生成事件文档
空数据拦截
无数据事件
泛型事件
自动清除空闲事件
事件强约束
生命周期感知
延迟发送事件
有序接收事件
订阅 Sticky 事件
清除 Sticky 事件
移除事件
线程调度
跨进程 / 跨 App (可支持)
关键原理 APT+静态代理 APT+动态代理 APT+静态代理 LiveData LiveData APT RxJava

2.2 ModularEventBus 特性一览

1、事件强约束

支持零配置快速使用;

支持 APT 注解处理器自动生成事件接口类;

支持编译时合法性校验和警告提示;

支持生成事件文档;

支持增量编译;

2、Lifecycle 生命周期感知

内置基于 LiveData 的 LiveDataBus;

支持自动取消订阅,避免内存泄漏;

支持安全地发送事件与接收事件,避免产生空指针异常或不必要的性能损耗;

支持永久订阅事件;

支持自动清除没有关联订阅者的空闲 LiveData 以释放内存;

3、更多特性支持

支持 Java / Kotlin;

支持 AndroidX;

支持订阅 Sticky 粘性事件,支持移除事件;

支持 Generic 泛型事件,如 List<String> 事件;

支持拦截空数据;

支持只发布事件不携带数据的无数据事件;

支持延迟发送事件;

支持有序接收事件。


3. ModularEventBus 快速使用

  • 1、添加依赖

模块级 build.gradle

plugins {
id 'com.android.application' // 或 id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
dependencies {
// 替换成最新版本
implementation 'io.github.pengxurui:modular-eventbus-api:1.0.4'
kapt 'io.github.pengxurui:modular-eventbus-compiler:1.0.4'
...
}
  • 2、定义事件数据类型(可选): 定义事件关联的数据类型,对于只发布事件而不需要携带数据的场景,可以不定义事件类型。

UserInfo.kt

data class UserInfo(val userName: String)
  • 3、定义事件: 使用接口定义事件名和事件数据类型,并使用 @EventGroup 注解修饰该接口:

LoginEvents.kt

@EventGroup
interface LoginEvents { // 事件名:login
// 事件数据类型:UserInfo
fun login(): UserInfo // 事件名:logout
fun logout()
}
  • 4、执行注解处理器: 执行 Make ProjectRebuild Project 等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如,LoginEvents 对应的事件类为:

EventDefineOfLoginEvents.java

/**
* Auto generate code, do not modify!!!
* @see com.pengxr.sampleloginlib.events.LoginEvents
*/
@SuppressWarnings("unchecked")
public class EventDefineOfLoginEvents implements IEventGroup {
private EventDefineOfLoginEvents() {
} public static IEvent<UserInfo> login() {
return (IEvent<UserInfo>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$login", UserInfo.class, false, true));
} public static IEvent<Void> logout() {
return (IEvent<Void>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$logout", Void.class, true, false));
}
}
  • 5、订阅事件: 使用 EventDefineOfLoginEvents 事件类提供的静态方法订阅事件:

订阅者示例

// 以生命周期感知模式订阅事件(不需要手动注销订阅)
EventDefineOfLoginEvents.login().observe(this) { value: UserInfo? ->
// Do something.
} // 以永久模式订阅事件(需要手动注销订阅)
EventDefineOfLoginEvents.logout().observeForever { _: Void? ->
// Do something.
}
  • 6、发布事件: 使用 EventDefineOfLoginEvents 提供的静态方法发布事件:

发布者示例

EventDefineOfLoginEvents.login().post(UserInfo("XIAOPENG"))

EventDefineOfLoginEvents.logout().post(null)
  • 7、添加混淆规则(如果使用了 minifyEnabled true):
-dontwarn com.pengxr.modular.eventbus.generated.**
-keep class com.pengxr.modular.eventbus.generated.** { *; }
-keep @com.pengxr.modular.eventbus.facade.annotation.EventGroup class * {*;} # 可选

4. 完整使用文档

4.1 定义事件

  • 使用注解定义事件:

    • @EventGroup 注解: @EventGroup 注解用于定义事件组,修饰于 interface 接口上,在该类中定义的每个方法均视为一个事件定义;

    • @Event 注解: @Event 注解用于事件组中的事件定义,亦可省略。

模板程序如下:

com.pengxr.sample.events.MainEvents.kt

// 事件组
@EventGroup
interface MainEvents { // 事件
// @Event 可以省略
@Event
fun open(): String
}

提示: 以上即定义了一个 MainEvents 事件组,其中包含一个 com.pengxr.sample.events.MainEvents$$open 事件且数据类型为 String 类型。

亦兼容将 @EventGroup 修饰于 class 类而非 interface 接口,但会有编译时警告: Annotated @EventGroup on a class type [IllegalEvent], expected a interface. Is that really what you want?

错误示例

@EventGroup
class IllegalEvent { fun illegalEvent() { }
}
  • 使用 @Ignore 注解忽略定义: 使用 @Ignore 注解可以排除事件类或事件方法,使其不被视为事件定义。

示例程序

// 可以修饰于事件组
@Ignore
@EventGroup
interface IgnoreEvent { // 亦可修饰于事件
@Ignore
fun ignoredMethod() fun method()
}
  • 使用 @Deprecated 注解提示过时: 使用 @Deprecated 注解可以标记事件为过时。与 @Ignore 不同是,@Deprecated 修饰的类或方法依然是有效的事件定义。

示例程序

// 虽然过时,但依然是有效的事件定义
@Deprecated("Don't use it.")
@EventGroup
interface DeprecatedEvent { @Deprecated("Don't use it.")
fun deprecatedMethod()
}
  • 定义事件数据类型: 事件方法返回值即表示事件数据类型,支持泛型(如 List<String>),支持不携带数据的无数据事件。以下均为合法定义:

Java 示例程序

// 事件数据类型为 String
String stringEventInJava(); // 事件数据类型为 List<String>
List<String> listEventInJava(); // 以下均视为无数据事件
void voidEventInJava1();
Void voidEventInJava2();

Kotlin 示例程序

// 事件数据类型为 String
fun stringEventInKotlin(): String // 事件数据类型为 List<String>
fun listEventInKotlin(): List<String> // 以下均视为无数据事件
fun voidEventInKotlin1()
fun voidEventInKotlin2(): Unit
fun voidEventInKotlin3(): Unit?
  • 定义事件数据可空性: 使用 @Nullable@NonNull 注解表示事件数据可空性,默认为可空类型。以下均为合法定义:

Java 示例程序

@NonNull
String nonNullEventInJava(); @Nullable
String nullableEventInJava(); // 默认视为 @Nullable
String eventInJava();

Kotlin 示例程序

fun nonNullEventInKotlin(): String

// 提示:Kotlin 编译器将返回类型上的 ? 号视为 @org.jetbrains.annotations.Nullable
fun nullableEventInKotlin(): String?

以下为支持的可空性注解:

org.jetbrains.annotations.Nullable
android.annotation.Nullable
androidx.annotation.Nullable org.jetbrains.annotations.NotNull
android.annotation.NonNull
androidx.annotation.NonNull
  • 定义自动清除事件: 支持配置在事件没有关联的订阅者时自动被清除(以释放内存),默认值为 false。可以使用 @EventGroup 注解或 @Event 注解进行修改,以 @Event 的取值优先。

示例程序

@EventGroup(autoClear = true)
interface MainEvents { @Event(autoClear = false)
fun normalEvent(): String // 继承 @EventGroup 中的 autoClear 取值
fun autoClearEvent(): String
}
  • 定义事件所属组件名: 为避免不同组件中的事件名重复,框架自动使用 "[moduleName]$$[eventName]" 作为最终的事件名。默认使用事件组的 [全限定类名] 作为 moduleName,可以使用 @EventGroup 注解进行修改。

示例程序

com.pengxr.sample.events.MainEvents.kt

@EventGroup(moduleName = "main")
interface MainEvents { fun open(): String
}

提示: 以上即定义了一个 MainEvents 事件组,其中包含一个 main$$open 事件且数据类型为 String 类型。

4.2 执行注解处理器

在完成事件定义后,执行 Make ProjectRebuild Project 等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如, MainEvents 对应的事件接口为:

com.pengxr.modular.eventbus.generated.events.com.pengxr.sample.events.EventDefineOfMainEvents.java

/**
* Auto generate code, do not modify!!!
* @see com.pengxr.sample.events.MainEvents
*/
@SuppressWarnings("unchecked")
public class EventDefineOfMainEvents implements IEventGroup {
private EventDefineOfMainEvents() {
} public static IEvent<String> open() {
return (IEvent<String>) (ModularEventBus.INSTANCE.createObservable("main$$open", String.class, false, false));
}
}

EventDefineOfMainEvents 中的静态方法与 MainEvent 事件组中的每个事件一一对应,直接通过静态方法即可获取事件实例,而不再通过手动输入事件名字符串或事件数据类型,故可避免事件名错误或数据类型错误等问题。

所有的事件实例均是 IEvent 泛型接口的实现类,例如 open 事件属于 IEvent<String> 类型的事件实例。发布事件和订阅事件需要用到 IEvent 接口中定义的一系列 post 方法和 observe 方法,IEvent 接口的完整定义如下:

IEvent.kt

interface IEvent<T> {

    /**
* 发布事件,允许在子线程发布
*/
@AnyThread
fun post(value: T?) /**
* 延迟发布事件,允许在子线程发布
*/
@AnyThread
fun postDelay(value: T?, delay: Long) /**
* 延迟发布事件,在准备发布前会检查 producer 处于活跃状态,允许在子线程发布
*
* @param producer 发布者的 LifecycleOwner
*/
@AnyThread
fun postDelay(value: T?, delay: Long, producer: LifecycleOwner) /**
* 发布事件,允许在子线程发布,确保订阅者按照发布顺序接收事件
*/
@AnyThread
fun postOrderly(value: T?) /**
* 以生命周期感知模式订阅事件(不需要手动注销订阅)
*/
@AnyThread
fun observe(consumer: LifecycleOwner, observer: Observer<T?>) /**
* 以生命周期感知模式粘性订阅事件(不需要手动注销订阅)
*/
@AnyThread
fun observeSticky(consumer: LifecycleOwner, observer: Observer<T?>) /**
* 以永久模式订阅事件(需要手动注销订阅)
*/
fun observeForever(observer: Observer<T?>) /**
* 以永久模式粘性订阅事件(需要手动注销订阅)
*
* @param observer Event observer.
*/
@AnyThread
fun observeStickyForever(observer: Observer<T?>) /**
* 注销订阅者
*/
@AnyThread
fun removeObserver(observer: Observer<T?>) /**
* 移除事件,关联的订阅者关系也会被解除
*/
@AnyThread
fun removeEvent()
}

4.3 订阅事件

使用 IEvent 接口定义的一系列 observe() 接口订阅事件,使用示例:

示例程序

// 以生命周期感知模式订阅(不需要手动注销订阅)
EventDefineOfMainEvents.open().observe(this) {
// do something.
} // 以生命周期感知模式、且粘性模式订阅(不需要手动注销订阅)
EventDefineOfMainEvents.open().observeSticky(this) {
// do something.
} val foreverObserver = Observer<String?> {
// do something.
} // 以永久模式订阅(需要手动注销订阅)
EventDefineOfMainEvents.open().observeForever(foreverObserver) // 以永久模式,且粘性模式订阅(需要手动注销订阅)
EventDefineOfMainEvents.open().observeStickyForever(foreverObserver) // 移除观察者
EventDefineOfMainEvents.open().removeObserver(foreverObserver)

4.4 发布事件

使用 IEvent 接口定义的一系列 post() 接口发布事件,使用示例:

示例程序

// 发布事件,允许在子线程发布
EventDefineOfMainEvents.open().post("XIAO PENG") // 延迟发布事件,允许在子线程发布
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000) // 延迟发布事件,在准备发布前会检查 producer 处于活跃状态,允许在子线程发布。
EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000, this) // 发布事件,允许在子线程发布,确保订阅者按照发布顺序接收事件
EventDefineOfMainEvents.open().postOrderly("XIAO PENG") // 移除事件
EventDefineOfMainEvents.open().removeEvent()

4.5 更多功能

  • 生成事件文档(可选): 支持生成事件文档,需要在 Gradle 配置中开启:

模块级 build.gradle

// 需要生成事件文档的模块就增加配置:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
MODULAR_EVENTBUS_GENERATE_DOC: "enable",
MODULAR_EVENTBUS_MODULE_NAME : project.getName()
]
}
}
}
}

文档生成路径: build/generated/source/kapt/[buildType]/com/pengxr/modular/eventbus/generated/docs/eventgroup-of-[MODULAR_EVENTBUS_MODULE_NAME].json

  • 配置(可选):

    • debug(Boolean): 调试模式开关;
    • throwNullEventException(Boolean): 非空事件发布空数据时是否抛出 NullEventException 异常,在 release 模式默认为只拦截不抛出异常,在 debug 模式默认为拦截且抛出异常;
    • setEventListener(IEventListener): 全局监听接口。

示例程序

ModularEventBus.debug(true)
.throwNullEventException(true)
.setEventListener(object : IEventListener {
override fun <T> onEventPost(eventName: String, event: BaseEvent<T>, data: T?) {
Log.i(TAG, "onEventPost: $eventName, event = $event, data = $data")
}
})

5. 未来功能规划

  • 支持跨进程 / 跨 App:LiveEventBus 框架支持跨进程 / 跨 App,未来根据使用反馈考虑实现该 Feature;
  • 支持替换内部 EventBus 工厂:ModularEventBus 已预设计事件总线工厂 IEventFactory,未来根据使用反馈考虑公开该 API;
  • 支持基于 Kotlin Flow 的 IEventFactory 工厂;
  • 编译时检查在不同 @EventGroup 中设置相同 modulaName 且相同 eventName,但事件数据类型不同的异常。

6. 共同成长

  • 欢迎提 Issue 帮助修复缺陷;
  • 欢迎提 Pull Request 增加新的 Feature,让 ModularEventBus 变得更加强大,你的 ID 会出现在 Contributors 中;
  • 欢迎加 作者微信 与作者交流,欢迎加入交流群找到志同道合的伙伴

参考资料

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。

美团组件化事件总线方案改进:ModularEventBus的更多相关文章

  1. Atitit..组件化事件化的编程模型--(2)---------Web datagridview 服务器端控件的实现原理and总结

    Atitit..组件化事件化的编程模型--(2)---------Web datagridview 服务器端控件的实现原理and总结 1. 服务端table控件的几个流程周期 1 1.1. 确认要显示 ...

  2. atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系

    atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系 1. 服务器控件是可被服务器理解的标签.有三种类型的服务器控件: 1 1.1. HTML 服务器控件  ...

  3. Android组件化之终极方案

    Android组件化项目地址:Android组件化项目AndroidModulePattern Fragment或View如何支持组件化 如何管理组件 Fragment或View如何支持组件化 距离 ...

  4. [Android Pro] 终极组件化框架项目方案详解

    cp from : https://blog.csdn.net/pochenpiji159/article/details/78660844 前言 本文所讲的组件化案例是基于自己开源的组件化框架项目g ...

  5. vue兄弟组件传值——事件总线

    1.创建一个js文件,例如msg.js,放到合适位置,例如components中,或者其他位置也行.然后在兄弟两个组件中分别引入msg.js文件 msg.js: import Vue from 'vu ...

  6. Android组件化方案

    Android组件化项目地址:Android组件化项目AndroidModulePattern Android组件化之终极方案地址:http://blog.csdn.net/guiying712/ar ...

  7. vue.js组件化开发实践

    前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了下面的内容.借油开车. 组件化 需求一到,接就是怎么实现,技术选型自然 ...

  8. .Net 事件总线之Autofac解耦

    事件总线是通过一个中间服务,剥离了常规事件的发布与订阅(消费)强依赖关系的一种技术实现.事件总线的基础知识可参考圣杰的博客[事件总线知多少] 本片博客不再详细概述事件总线基础知识,核心点放置使用Aut ...

  9. Autofac解耦事件总线

    事件总线之Autofac解耦 事件总线是通过一个中间服务,剥离了常规事件的发布与订阅(消费)强依赖关系的一种技术实现.事件总线的基础知识可参考圣杰的博客[事件总线知多少] 本片博客不再详细概述事件总线 ...

随机推荐

  1. SpringBoot整合SpringSecurityOauth2实现鉴权-动态权限

    写在前面 思考:为什么需要鉴权呢? 系统开发好上线后,API接口会暴露在互联网上会存在一定的安全风险,例如:爬虫.恶意访问等.因此,我们需要对非开放API接口进行用户鉴权,鉴权通过之后再允许调用. 准 ...

  2. 封装一个基础的vue-router

    前言主要知识点: 路由原理 Hash与History 实现路由 一.一个vue路由的工作原理前端路由与后端路由的区别: 后端路由:输入url>请求发送到服务器>服务器解析请求的路径> ...

  3. ABAP CDS - Language Elements

    The following sections summarize the language elements of the DDL and DCL of the ABAP CDS, arranged ...

  4. linux在线安装和配置JDK1.8

    首先在服务器ping www.baidu.com查看是否可以连网 然后就可以在线下载 一.下载安装JDK1.8 1.在下载安装的同时做一些准备工作 我们在usr目录下再创建一个Java文件夹准备放置我 ...

  5. XSS攻击(笔记)

    XSS攻击 XSS概述 XSS即跨站脚本攻击,(Cross-Site Scripting, CSS),但是为了与层叠样式表(Cascading Style Sheets, CSS)缩写区分开来,所以命 ...

  6. RPA 微信财务报销机器人 竹间智能

    1.首先通过微信对话机器人收集报销信息及内容 2.上传发票并进行OCR识别 3.收集相关的出差信息,支持对话中修改内容 4.完成信息收集后,后台RPA机器人执行报销操作,并发送确认邮件 5.收到邮件后 ...

  7. ConcurrentHashMap深入剖析(基于JDK1.7)

    最近有点时间,翻了翻ConcurrentHashMap的源码学习了一下,对我自己认为比较重要的一些方法进行了学习,添加了一些必要的注释,拿出来与园子的小伙伴分享一下,有说的不对的地方,还请各位批评指正 ...

  8. 一文看完vue3的变化之处

    在通读了vue的官网文档后,我记录下了如下这些相对于2.x的变化之处. 1.创建应用实例的变化 之前一般是这样: let app = new Vue({ // ...一些选项 template: '' ...

  9. ClickHouse(03)ClickHouse怎么安装和部署

    本文会介绍如何安装和部署ClickHouse,官方推荐的几种安装模式,以及安装之后如何启动,ClickHouse集群如何配置等. 简单来说,ClickHouse的搭建流程如下: 环境检查,环境依赖安装 ...

  10. 当在命令行输入"pip install xxx"

    当输入"pip install xxx"时发生了什么 不知道你在下载一些包的时候有没有什么疑惑,输入了"pip install xxx" ,系统是如何找到对应的 ...