• 从壹开始微服务 [ DDD ] 之十二 ║ 核心篇【下】:事件驱动EDA 详解


    缘起

    哈喽大家好,又是周二了,时间很快,我的第二个系列DDD领域驱动设计讲解已经接近尾声了,除了今天的时间驱动EDA(也有可能是两篇),然后就是下一篇的事件回溯,就剩下最后的权限验证了,然后就完结了,这两个月我也是一直在自学,然后再想栗子,个人感觉收获还是很大的,比如DDD领域分层设计、CQRS读写分离、CommandBus命令总线、EDA事件驱动、四色原理等等,如果大家真的能踏踏实实的看完,或者说多看看书,对个人的思想提高有很大的帮助,这里要说两点,可能会有一些小伙伴不开心,但是还是要说说:

    1、很多小伙伴一直问我看什么书,我个人感觉,只要是书看就对了,与其纠结哪本,还不如踏踏实实先看一本。

    2、还有小伙伴问,为啥还没有看到微服务的内容?

    我想说,其实微服务是一个很宽泛的领域,比如.net core的深入学习,依赖注入的使用,仓储契约、DDD+事件总线的学习、中介者模式、Docker的学习、容器化设计等等等等,这些都属于微服务的范畴,如果这些基础知识不会的话,可能是学不好微服务的。

    周末的时候,我又好好的整理了下我的Github上的代码,然后新建了一些分支(如果你不会使用Git命令,可以看我的一个文章:https://www.jianshu.com/p/2b666a08a3b5,会一直更新),主要是这样的(这个数字是对应的文章,比如今天的是第 12 ):

    其实我这个系列所说的 DDD领域驱动设计,是一个很丰富的概念,里边包含了DDD的多层设计思想、CQRS、Bus、EDA、ES等等,所以如果你只想要其中的一部分,可以对应的分支进行Clone,比如你单纯想要一个干净的基于DDD四层设计的模板,可以克隆 Framework_DDD_8 这个分支,如果你想带有读写分离,可以克隆 CQRS_DDD_9 这个分支等等,也方便好好研究。

    关于CQRS读写分离概念,请注意,分离不一定是分库,一个数据库也能实现读写分离,最简单的就是从Code上来区分。

    前言

    好啦,上边说了一些周末的思考,现在马上进入正文,不知道大家对上周的内容还有没有印象,主要用两篇文章来说明了命令总线的设计思想和执行过程《十 ║领域驱动【实战篇·中】:命令总线Bus分发(一)》、《十一 ║ 基于源码分析,命令分发的过程(二)》,咱们很好的实现了多个复杂模型间的解耦,成功的简化了API接口层和 Application应用服务层,把重心真正的转义到了领域层。

    当然其中也有一些新的问题出现了,这个也可以当作今天的每篇一问:

    首先,对领域通知的处理上,目前用的是通过一个 ErrorData 的key 来把错误通知放到了内存里,然后去读取,这样有一个很危险的问题,就是生命周期的问题,如果在当前实例中,没有及时删除,可能会出现错误通知的混乱,这是致命的,当然还有 key 的问题,因为几乎每一个 Command 都会有不同的信息,我们不能通过简简单单的人为取名字来实现这个逻辑,这是荒唐的。

    其次,如果我们 Command 执行完成,是如何发布通知的,比如注册成功的邮件,短信分发,站内推送等等。

    最后,不知道大家有没有深入的去学习,去了解 MediatR 中介者的两个模式:请求/响应模式 与 发布/订阅模式的区别和联系(详细的下边会说到)。

     你会说,很简单呀,我们直接在 CommandHandler 命令处理程序中处理不就行了,一步一步往下走就可以了呀,如果你现在还有这样的思维,那DDD可真的好好再学习了,为什么呢?很简单,我们当时为什么要把 contrller 的业务逻辑剥离到领域模型,就是为了业务独立化,不让多个不相干的业务缠绕(比如我们之前是把model 验证、错误返回、发邮件等,都是写在 controller 里的),那如果我们再把过多的业务逻辑写到命令处理程序中的话,那命令处理模型不就成为了第二个 controller 了么?我们为业务把 controller 剥离了一次,那今天咱们就继续从 命令处理程序中,再优化一次。

    零、今天要实现右下角蓝色的部分

    (周末有一个小伙伴问这个软件的地址:https://www.mindmeister.com,应该需要翻墙)

    一、领域事件驱动设计 —— EDA

    1、什么是领域事件 

    我们先看看官网,在《实现领域驱动设计》一书中对领域事件的定义如下:

    领域专家所关心的发生在领域中的一些事件。

    将领域中所发生的活动建模成一系列的离散事件。

    每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。

    领域事件:Domain Event,是针对某个业务来说的,或者说针对某个聚合的业务来说的,例如订单生成这种业务,它可以同时对应一种事件,比如叫做OrderGeneratorEvent,而你的零散业务可能随时会变,加一些业务,减一些业务,而对于订单生成这个事件来说,它是唯一不变的,而我们需要把这些由产生订单而发生变化的事情拿出来,而拿出来的这些业务就叫做"领域事件".其中的领域指的就是订单生成这个聚合;而事件指的就是那些零散业务的统称.

    2、领域事件包含了哪些内容

    如果你对上一篇命令总线很熟悉,这里就特别简单,几乎是一个模式,只不过总线发布的方式不一样罢了,如果你比较熟悉命令驱动,这里正好温习。如果不了解,这里就一起看吧,千万记得再回去看前两篇内容哟。

    在面向对象的编程世界里,做这种事情我们需要几个抽象:

    领域对象事件标示:标示接口,接口的一种,用来约束一批对象,IEvent(当前也可以使用抽象类,本文即是)

    领域对象的处理方法行为:比如 StudentEventHandler。(我们的命令处理程序也是如此)

    事件总线:事件处理核心类,承载了事件的发布,订阅与取消订阅的逻辑,EventBus(这个和我们的命令总线CommandBus很类似)

    某个领域对象的事件:它是一个事件处理类,它实现了 EventHandler,它所处理的事情需要在Handle里去完成

    一个领域事件可以理解为是发生在一个特定领域中的事件,是你希望在同一个领域中其他部分知道并产生后续动作的事件。一个领域事件必须对业务有价值,有助于形成完整的业务闭环,也即一个领域事件将导致进一步的业务操作。就比如我们今天说到的领域通知,就应该是一个事件,我们从命令中产生的错误提示,通过处理程序,引发到事件总线内,并返回到前台。

    3、为什么需要领域事件

    领域事件也是一种基于事件的架构(EDA)。事件架构的好处可以把处理的流程解耦,实现系统可扩展性,提高主业务流程的内聚性。

    在咱们文章的开头,可说到了这个问题,不知道大家是否还记得,咱们再分析一下:

    我们提交了一个添加Student 的申请,系统在完成保存后,可能还需要发送一个通知(当然这里错误信息,也有成功的),当然肯定还会会一些其他的后台服务的活动。如果把这一系列的动作放入一个处理过程中,会产生几个的明显问题:

    1、一个是命令提交的的事务比较长,性能会有问题,甚至在极端情况下容易引发数据库的严重故障(服务器方面);

    2、另外提交的服务内聚性差,可维护性差,在业务流程发生变更时候,需要频繁修改主程序(程序员方面)。

    3、我们有时候只关心核心的流程,就比如添加Student,我们只关心是否添加成功,而且我们需要对这个成功有反馈,但是发邮件的功能,我们却不用放在主业务中,甚至发送成功与否,不影响 Student 的正常添加,这样我们就把后续的这些活动事件,从主业务中剥离开,实现了高内聚和低耦合(业务方面)。

    还记得 MediatR 有两个中介者模式么:请求/响应 和 发布/订阅。在我们的系统中,添加一个学生命令,就是用到的请求/响应 IRequest 模式,因为我们需要等待当前操作完成,我们需要总线对我们的请求做出响应。

    但是有时候我们不需要在同一请求/响应中立即执行一个动作的结果,只要异步执行这个动作,比如发送电子邮件。在这种情况下,我们使用发布/订阅模式,以异步方式发送电子邮件,并避免让用户等待发送电子邮件。

    4、领域事件驱动是如何运行的呢?

    这个时候,就用到之前我画的图了,中介者模式下,上半部的命令总线已经说完,今天说另一半事件总线:

    当然这里也有一个网上的栗子,很不错:

     从图中我们也可以看到,事件驱动的工作流程呢,在命令模式下,主要是在我们的命令处理程序中出现,在我们对数据进行持久化操作的时候,作为一个后续活动事件来存在,比如我们今天要实现的两个处理工作:

    1、通知信息的收集(之前我们是采用的缓存 Memory 来实现的);

    2、领域通知处理程序(比如发邮件等);

     

    这个时候,如果你对事件驱动有了一定的理解的话,你就会问,那我们在项目中具体的应该使用呢,请往下看。

     

    二、创建事件总线

    这个整体流程其实和命令总线分发很像,所以原理就不分析了,相信你如果看了之前的两篇文章的话,一定能看懂今天的内容的。

    1、定义领域事件标识基类

    就如上边我们说到的,我们可以定义一个接口,也可以定义一个抽象类,我比较习惯用抽象类,在核心领域层 Christ3D.Domain.Core 中的Events 文件夹中,新建Event.cs 事件基类:

    namespace Christ3D.Domain.Core.Events
    {
        /// <summary>
        /// 事件模型 抽象基类,继承 INotification
        /// 也就是说,拥有中介者模式中的 发布/订阅模式
        /// </summary>
        public abstract class Event : INotification
        {
            // 时间戳
            public DateTime Timestamp { get; private set; }
            
            // 每一个事件都是有状态的
            protected Event()
            {
                Timestamp = DateTime.Now;
            }
        }
    }

    2、定义事件总线接口

    在中介处理接口IMediatorHandler中,定义引发事件接口,作为发布者,完整的 IMediatorHandler.cs 应该是这样的

    namespace Christ3D.Domain.Core.Bus
    {
        /// <summary>
        /// 中介处理程序接口
        /// 可以定义多个处理程序
        /// 是异步的
        /// </summary>
        public interface IMediatorHandler
        {
            /// <summary>
            /// 发送命令,将我们的命令模型发布到中介者模块
            /// </summary>
            /// <typeparam name="T"> 泛型 </typeparam>
            /// <param name="command"> 命令模型,比如RegisterStudentCommand </param>
            /// <returns></returns>
            Task SendCommand<T>(T command) where T : Command;
    
            /// <summary>
            /// 引发事件,通过总线,发布事件
            /// </summary>
            /// <typeparam name="T"> 泛型 继承 Event:INotification</typeparam>
            /// <param name="event"> 事件模型,比如StudentRegisteredEvent,</param>
            /// 请注意一个细节:这个命名方法和Command不一样,一个是RegisterStudentCommand注册学生命令之前,一个是StudentRegisteredEvent学生被注册事件之后
            /// <returns></returns>
            Task RaiseEvent<T>(T @event) where T : Event;
    
        }
    }

     

    3、实现总线分发接口

     在基层设施总线层的记忆总线 InMemoryBus.cs 中,实现我们上边的事件分发总线接口:

     /// <summary>
     /// 引发事件的实现方法
     /// </summary>
     /// <typeparam name="T">泛型 继承 Event:INotification</typeparam>
     /// <param name="event">事件模型,比如StudentRegisteredEvent</param>
     /// <returns></returns>
     public Task RaiseEvent<T>(T @event) where T : Event
     {
         // MediatR中介者模式中的第二种方法,发布/订阅模式
         return _mediator.Publish(@event);
     }

    注意这里使用的是中介模式的第二种——发布/订阅模式,想必这个时候就不用给大家解释为什么要使用这个模式了吧(提示:不需要对请求进行必要的响应,与请求/响应模式做对比思考)。现在我们把事件总线定义(是一个发布者)好了,下一步就是如何定义事件模型和处理程序了也就是订阅者,如果上边的都看懂了,请继续往下走。

    三、事件模型的处理与使用

     可能这句话不是很好理解,那说人话就是:我们之前每一个领域模型都会有不同的命令,那每一个命令执行完成,都会有对应的后续事件(比如注册和删除用户肯定是不一样的),当然这个是看具体的业务而定,就比如我们的订单领域模型,主要的有下单、取消订单、删除订单等。

    我个人感觉,每一个命令模型都会有对应的事件模型,而且一个命令处理方法可能有多个事件方法。具体的请看:

    1、定义添加Student 的事件模型

    当然还会有删除和更新的事件模型,这里就用添加作为栗子,在领域层 Christ3D.Domain 中,新建  Events 文件夹,用来存放我们所有的事件模型,

    因为是 Student 模型,所以我们在 Events 文件夹下,新建 Student 文件夹,并新建 StudentRegisteredEvent.cs 学生添加事件类:

    namespace Christ3D.Domain.Events
    {
        /// <summary>
        /// Student被添加后引发事件
        /// 继承事件基类标识
        /// </summary>
        public class StudentRegisteredEvent : Event
        {
            // 构造函数初始化,整体事件是一个值对象
            public StudentRegisteredEvent(Guid id, string name, string email, DateTime birthDate, string phone)
            {
                Id = id;
                Name = name;
                Email = email;
                BirthDate = birthDate;
                Phone = phone;
            }
            public Guid Id { get; set; }
            public string Name { get; private set; }
            public string Email { get; private set; }
            public DateTime BirthDate { get; private set; }
            public string Phone { get; private set; }
        }
    }

    2、定义领域事件的处理程序Handler

    这个和我们的命令处理程序一样,只不过我们的命令处理程序是总线在应用服务层分发的,而事件处理程序是在领域层的命令处理程序中被总线引发的,可能有点儿拗口,看看下边代码就清楚了,就是一个引用场景的顺序问题。

    在领域层Chirst3D.Domain 中,新建 EventHandlers 文件夹,用来存放我们的事件处理程序,然后新建 Student事件模型的处理程序 StudentEventHandler.cs:

    namespace Christ3D.Domain.EventHandlers
    {
        /// <summary>
        /// Student事件处理程序
        /// 继承INotificationHandler<T>,可以同时处理多个不同的事件模型
        /// </summary>
        public class StudentEventHandler :
            INotificationHandler<StudentRegisteredEvent>,
            INotificationHandler<StudentUpdatedEvent>,
            INotificationHandler<StudentRemovedEvent>
        {
            // 学习被注册成功后的事件处理方法
            public Task Handle(StudentRegisteredEvent message, CancellationToken cancellationToken)
            {
                // 恭喜您,注册成功,欢迎加入我们。
    
                return Task.CompletedTask;
            }
    
            // 学生被修改成功后的事件处理方法
            public Task Handle(StudentUpdatedEvent message, CancellationToken cancellationToken)
            {
                // 恭喜您,更新成功,请牢记修改后的信息。
    
                return Task.CompletedTask;
            }
    
            // 学习被删除后的事件处理方法
            public Task Handle(StudentRemovedEvent message, CancellationToken cancellationToken)
            {
                // 您已经删除成功啦,记得以后常来看看。
    
                return Task.CompletedTask;
            }
        }
    }

    相信大家应该都能看的明白,在上边的注释已经很清晰的表达了响应的作用,如果有看不懂,咱们可以一起交流。

    好啦,现在第二步已经完成,剩下最后一步:如何通过事件总线分发我们的事件模型了。

    3、在事件总线EventBus中引发事件

    这个使用起来很简单,主要是我们在命令处理程序中,处理完了持久化以后,接下来调用我们的事件总线,对不同的事件模型进行分发,就比如我们的 添加Student 命令处理程序方法中,我们通过工作单元添加成功后,需要做下一步,比如发邮件,那我们就需要这么做。

    在命令处理程序 StudentCommandHandler.cs 中,完善我们的提交成功的处理:

     // 持久化
     _studentRepository.Add(customer);
    
     // 统一提交
     if (Commit())
     {
         // 提交成功后,这里需要发布领域事件
         // 比如欢迎用户注册邮件呀,短信呀等
         Bus.RaiseEvent(new StudentRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate,customer.Phone));
     }

    这样就很简单的将我们的事件模型分发到了事件总线中去了,这个时候记得要在 IoC 原生注入类NativeInjectorBootStrapper中,进行注入。关于触发过程下边我简单说一下。

    4、整体事件驱动执行过程

     说到了这里,你可能发现和命令总线很相似,也可能不是很懂,简单来说,整体流程是这样的:

    1、首先我们在命令处理程序中调用事件总线来引发事件  Bus.RaiseEvent(........);

    2、然后在Bus中,将我们的事件模型进行包装成固定的格式   _mediator.Publish(@event);

    3、然后通过注入的方法,将包装后的事件模型与事件处理程序进行匹配,系统执行事件模型,就自动实例化事件处理程序 StudentEventHandler;

    4、最后执行我们Handler 中各自的处理方法 Task Handle(StudentRegisteredEvent message)。

    希望正好也温习下命令总线的执行过程。

    5、依赖注入事件模型和处理程序

     // Domain - Events
     // 将事件模型和事件处理程序匹配注入
     services.AddScoped<INotificationHandler<StudentRegisteredEvent>, StudentEventHandler>();
     services.AddScoped<INotificationHandler<StudentUpdatedEvent>, StudentEventHandler>();
     services.AddScoped<INotificationHandler<StudentRemovedEvent>, StudentEventHandler>();

    这个时候,我们DDD领域驱动设计核心篇的第一部分就是这样了,还剩下最后的,事件驱动的事件源事件存储/回溯,我们下一讲再说。

    接下来咱们说说领域通知,为什么要说领域通知呢,大家应该还记得我们之前将错误信息放到了内存中,无论是操作还是业务上都很严重的问题,肯定是不可取的。那我们应该采用什么办法呢,欸?!没错,你会发现,通过上边的事件驱动设计,发现领域通知我们也可以采用这个方法,首先是多个模型之间相互通讯,但又不相互引用;而且也在命令处理程序中,对信息进行分发,和发邮件很类似,那具体如何操作呢,请往下看。

    四、事件分发的另一个用途 —— 领域通知

    1、领域通知模型 DomainNotification 

     这个通知模型,就像是一个消息队列一样,在我们的内存中,通过通知处理程序进行发布和使用,有自己的生命周期,当被访问并调用完成的时候,会手动对其进行回收,以保证数据的完整性和一致性,这个就很好的解决了咱们之前用Memory缓存通知信息的弊端。

    在我们的核心领域层 Christ3D.Domain.Core 中,新建文件夹 Notifications ,然后添加领域通知模型 DomainNotification.cs:

    namespace Christ3D.Domain.Core.Notifications
    {
        /// <summary>
        /// 领域通知模型,用来获取当前总线中出现的通知信息
        /// 继承自领域事件和 INotification(也就意味着可以拥有中介的发布/订阅模式)
        /// </summary>
        public class DomainNotification : Event
        {
            // 标识
            public Guid DomainNotificationId { get; private set; }
            // 键(可以根据这个key,获取当前key下的全部通知信息)
            // 这个我们在事件源和事件回溯的时候会用到,伏笔
            public string Key { get; private set; }
            // 值(与key对应)
            public string Value { get; private set; }
            // 版本信息
            public int Version { get; private set; }
    
            public DomainNotification(string key, string value)
            {
                DomainNotificationId = Guid.NewGuid();
                Version = 1;
                Key = key;
                Value = value;
            }
        }
    }

     

     

    2、领域通知处理程序 DomainNotificationHandler

    该处理程序,可以理解成,就像一个类的管理工具,在每次对象生命周期内 ,对领域通知进行实例化,获取值,手动回收,这样保证了每次访问的都是当前实例的数据。

     还是在文件夹 Notifications 下,新建处理程序 DomainNotificationHandler.cs:

    namespace Christ3D.Domain.Core.Notifications
    {
        /// <summary>
        /// 领域通知处理程序,把所有的通知信息放到事件总线中
        /// 继承 INotificationHandler<T>
        /// </summary>
        public class DomainNotificationHandler : INotificationHandler<DomainNotification>
        {
            // 通知信息列表
            private List<DomainNotification> _notifications;
    
            // 每次访问该处理程序的时候,实例化一个空集合
            public DomainNotificationHandler()
            {
                _notifications = new List<DomainNotification>();
            }
    
            // 处理方法,把全部的通知信息,添加到内存里
            public Task Handle(DomainNotification message, CancellationToken cancellationToken)
            {
                _notifications.Add(message);
                return Task.CompletedTask;
            }
            
            // 获取当前生命周期内的全部通知信息
            public virtual List<DomainNotification> GetNotifications()
            {
                return _notifications;
            }
    
            // 判断在当前总线对象周期中,是否存在通知信息
            public virtual bool HasNotifications()
            {
                return GetNotifications().Any();
            }
    
            // 手动回收(清空通知)
            public void Dispose()
            {
                _notifications = new List<DomainNotification>();
            }
        }
    }

    到了目前为止,我们的DDD领域驱动设计中的核心领域层部分,已经基本完成了(还剩下下一篇的事件源、事件回溯):

     

    3、在命令处理程序中发布通知

     我们定义好了领域通知的处理程序,我们就可以像上边的发布事件一样,来发布我们的通知信息了。这里用一个栗子来试试:

    在学习命令处理程序 StudentCommandHandler.cs 中的 RegisterStudentCommand 处理方法中,完善:

     // 判断邮箱是否存在
     // 这些业务逻辑,当然要在领域层中(领域命令处理程序中)进行处理
     if (_studentRepository.GetByEmail(customer.Email) != null)
     {
         ////这里对错误信息进行发布,目前采用缓存形式
         //List<string> errorInfo = new List<string>() { "该邮箱已经被使用!" };
         //Cache.Set("ErrorData", errorInfo);
    
         //引发错误事件
         Bus.RaiseEvent(new DomainNotification("", "该邮箱已经被使用!"));
         return Task.FromResult(new Unit());
     }

    这个时候,我们把错误通知信息在事件总线中发布出去,剩下的就是需要在别的任何地方订阅即可,还记得哪里么,没错就是我们的自定义视图组件中,我们需要订阅通知信息,展示在页面里。

    注意:我们还要修改一下之前我们的命令处理程序基类 CommandHandler.cs 的验证信息收集方法,因为之前是用缓存来实现的,我们这里也用发布事件来实现:

     //将领域命令中的验证错误信息收集
     //目前用的是缓存方法(以后通过领域通知替换)
     protected void NotifyValidationErrors(Command message)
     {
         List<string> errorInfo = new List<string>();
         foreach (var error in message.ValidationResult.Errors)
         {
             //errorInfo.Add(error.ErrorMessage);      
             //将错误信息提交到事件总线,派发出去
             _bus.RaiseEvent(new DomainNotification("", error.ErrorMessage));
         }
         //将错误信息收集一:缓存方法(错误示范)
         //_cache.Set("ErrorData", errorInfo);
     }

    4、在视图组件中获取通知信息

    这个很简单,之前我们用的是注入 IMemory 的方式,在缓存中获取,现在我们通过注入领域通知处理程序来实现,在视图组件 AlertsViewComponent.cs 中:

        public class AlertsViewComponent : ViewComponent
        {
            // 缓存注入,为了收录信息(错误方法,以后会用通知,通过领域事件来替换)
            // private IMemoryCache _cache;
            // 领域通知处理程序
            private readonly DomainNotificationHandler _notifications;
    
            // 构造函数注入
            public AlertsViewComponent(INotificationHandler<DomainNotification> notifications)
            {
                _notifications = (DomainNotificationHandler)notifications;
            }
    
            /// <summary>
            /// Alerts 视图组件
            /// 可以异步,也可以同步,注意方法名称,同步的时候是Invoke
            /// 我写异步是为了为以后做准备
            /// </summary>
            /// <returns></returns>
            public async Task<IViewComponentResult> InvokeAsync()
            {
                // 从通知处理程序中,获取全部通知信息,并返回给前台
                var notificacoes = await Task.FromResult((_notifications.GetNotifications()));
                notificacoes.ForEach(c => ViewData.ModelState.AddModelError(string.Empty, c.Value));
    
                return View();
            }
        }

    5、StudentController 判断是否有通知信息

     通过注入的方式,把 INotificationHandler<DomainNotification> 注入控制器,然后因为这个接口可以实例化多个对象,那我们就强类型转换成 DomainNotificationHandler:

    这里要说明下,记得要对事件处理程序注入,才能使用:

     // 将事件模型和事件处理程序匹配注入
     services.AddScoped<INotificationHandler<DomainNotification>, DomainNotificationHandler>();

    五、结语

    好啦,今天的讲解基本就到这里了,今天重点说明了,我们如何使用事件总线,已经事件驱动模型下如何定义事件模型和事件处理程序,如果你都看懂了呢,这里可以简单回想一下以下几个问题:

    1、为什么要定义事件驱动呢?(提示词:业务分离)

    2、我们是在哪里发布这些事件的呢?(提示词:.publish()方法) 

    3、事件驱动中的生命周期是从哪里开始到哪里接受的?(提示:处理程序Handler)

     

    如果你对以上的内容还是比较困惑呢,这里有两个文章可以参考,当然,多沟通才是关键!

    https://www.cnblogs.com/lori/p/4080426.html

    https://blog.csdn.net/sD7O95O/article/details/79609305

     

    六、GitHub & Gitee

    https://github.com/anjoy8/ChristDDD

    https://gitee.com/laozhangIsPhi/ChristDDD 

     

     

    --END

  • 相关阅读:
    灭霸冲刺(7)
    灭霸冲刺(6)
    灭霸冲刺(5)
    灭霸冲刺(4)
    pip安装软件或模块时提示cannot import name 'main'
    mysql的安装与使用
    channels 2.x的使用
    charlesmitmproxyappium的安装与使用
    centos中redis安装
    django+nginx+gunicorn+pipenv微信小程序实践笔记
  • 原文地址:https://www.cnblogs.com/laozhang-is-phi/p/10059878.html
Copyright © 2020-2023  润新知