DDD 领域驱动设计-如何 DDD?

435188-20160414100345254-1897931146.jpg

注:科比今天要退役了,我是 60 亿分之一,满腹怀念~???

前几天看了园友的一篇文章《我眼中的领域驱动设计》,文中有段话直击痛点:有人误认为项目架构中加入 Repository,Domain,ValueObject 就变成了 DDD 架构。没错,我就是这样,不过准确的来说,并不能称为 DDD 架构,而是我之前经常说的“伪 DDD”设计,后来我还抽离出了一个伪 DDD 设计框架:DDD.Sample,大家有兴趣的可以瞧瞧,在实际项目的开发中,我用它做过了几个不太大的项目,我个人觉得用起来很顺手,当然并没有真正的 DDD 设计,只不过用了一个空的架构而已,然后冠以”DDD 开发“的名号。

因为之前有过 DDD 设计的痛苦经历(短消息系统),具体表现就是,如果真正 DDD 设计,需要花费大量的时间和精力,可能几天都在思考一个问题或者考虑几行代码的编写,但最后也可能没什么结论或结果,并且这个过程是很艰难和痛苦的,所以我后来就变懒了,懒的去思考项目所表现的业务,也不再考虑如何去设计领域模型,只是在考虑如何让框架用起来更爽,DDD.Sample 前两个应用的实际项目,我都是在完善这个框架,比如 Repository 和 UnitOfWork 的设计等等,所以,关于领域模型的设计,就是一堆贫血模型。不过,后来应用的第三个项目,也就是上一个实际项目,我觉得不能再这样下去了,因为没啥意义,框架一遍一遍的套用,而 DDD 却和它没半毛钱关系,所以,我就花了点时间去思考(只是简单的思考):我做这个项目的核心业务是什么?我该如何提炼出核心业务?提炼出核心业务之后,其他的任何实现都是为核心业务服务的,所以,你可以把这个核心业务看成领域模型。

关于第三个应用项目,实际上就是我们园子的“提到我”系统,现在已经应用在新闻评论了,大家可以去瞧瞧,类似为微博的“提到我”,相对比较简单的一个系统,你可以在评论中 @一个人,然后另一个人会接受通知,那这个系统的核心业务是什么?其实就是上面那句话,只不过你需要抽离出一些内容,如果领域专家和开发人员进行交流这个系统的设计,那领域专家的表述就是:你可以在评论中 @一个人,然后另一个人会接受通知,领域专家可能不懂代码设计,他的这个表述就是最直接和最精准的业务,所以,我们需要针对这段话,再和领域专家深入探讨下所蕴含的业务:

  • 你可以在评论中 @一个人 -> @一个人 -> 怎么能得到并确认这个“一个人” -> @匹配规则
  • 另一个人会接受通知 -> 通知 -> 通知所 @的人

所以,@匹配规则通知所 @的人是“提到我”系统的核心业务,确定好核心业务了,那就该具体的实现了,关于这个我没有深入的去考虑,就直接放在了 Mention(提到)中,大致代码:

namespace CNBlogs.Mention.Domain
{public class Mention : IAggregateRoot{private static readonly string SplitRegex = "::  @,";private static readonly Regex MentionsRegex = new Regex($"@([^{SplitRegex}]+)", RegexOptions.Compiled);public int Id { get; set; }public string Content { get; set; }public int ContentId { get; set; }public AppType AppType { get; set; }...//抽离public async Task<List<int>> Extract(){...}//通知public async Task Notify(){...}}
}

看起来很简单,就是把两个方法放在了 Mention 中,但这简单的操作却好像给 Mention 领域模型生命一样,不再那么贫血,对于复杂系统的业务变化,往往是核心业务的变化,其他的都是为核心业务服务的业务流程,并不能真正称为业务,比如 Application 层的代码,现在领域专家说 @一个人的规则需要改变,或者通知规则需要变化,我们只需要修改 Mention 领域模型的代码就行了,其他的代码并不需要修改,这就是 DDD 设计最浅显的体现。

大致贴下 Application 层的伪代码:

namespace CNBlogs.Mention.Application.Services
{public class MentionService : IMentionService{private IMentionRepository _mentionRepository;private IUnitOfWork _unitOfWork;public MentionService(IUnitOfWork unitOfWork,IMentionRepository mentionRepository){_unitOfWork = unitOfWork;_mentionRepository = mentionRepository;}public async Task<SubmitResult> Submit(string content ,int contentId, AppType appType){var notifyMentions = new List<Domain.Mention>();var existingQuery = _mentionRepository.Get(contentId, appType);var mention = new Domain.Mention(){Content = content,ContentId = contentId,AppType = appType};var userIds = await mention.Extract();foreach (var userId in userIds){var userQuery = existingQuery.Where(x => x.UserId == userId);if (await userQuery.AnyAsync()){await userQuery.UpdateAsync(x => new Domain.Mention { Content = content, DateUpdated = DateTime.Now });}else{mention.UserId = userId;_unitOfWork.RegisterNew(mention);notifyMentions.Add(mention);}}if (await _unitOfWork.CommitAsync()){foreach (var notifyMention in notifyMentions){await notifyMention.Notify();}return new SubmitResult();}return new SubmitResult { IsSucceed = false };}}
}

可以看到,Submit 中的操作基本上都是工作流程,先抽取用户,再进行判断更新,然后进行持久化,最后进行通知,没有任何业务体现,所以,如果核心业务发生了变化,这部分的代码并不需要随之改变。

如何 DDD?

151808224336247.png

引自:《Implementing DDD Reading - Strategic Design》

如何 DDD?其实答案都在上面的图中,图中的设计在《实现领域驱动设计》书中,被定义为战略建模(Strategic Modeling),主要包含领域、核心域、子域、通用子域、支撑子域、限界上下文、协作上下文、上下文映射图等等概念,我之前的几篇《IDDD 实现领域驱动设计》系统文章,也有过相关的介绍,说实话,我只是当时读过写过有些记忆,现在让我再说任何一个概念,基本上我说不上来,对于我个人来说,战略建模是一种宏观的建模方式,你需要站在高处去俯瞰整个系统,并需要抽离出系统所包含的业务,并将它们一一划分,这个工作是非常难的,推荐几篇战略建模相关的文章:

  • 初窥 Bounded Context
  • 如何识别 Bounded Context
  • 领域驱动设计实战--战略建模

除了战略建模,还有一种建模方式叫战术建模(Tactical Modeling),主要包含聚合(Aggregate)、实体(Entity)、值对象(Value Objects)、资源库(Repository)、领域服务(Domain Services)、领域事件(Domain Events)、模块(Modules)等等概念。

在《实现领域驱动设计》书中,Scrum 团队(一个实际项目的开发团队)一开始就是采用的战术建模,并且在开发的过程中,他们并不知道战略建模是什么?最后导致了很多问题,书中有个节点就专门讲了“战略设计为什么重要?”,但我个人觉得,战略建模的重要也只是相对而言,它在应对大型复杂性的业务系统设计中,可以充分发挥它的特点,但针对一些相对简单的系统,还不如直接进行战术建模,比如上面说的“提到我”系统。

所以,目前来说,进行战术建模比较现实和有意义,但在进行战术建模之前,我觉得还有一个重要的工作,就是和领域专家进行交流系统业务,这个工作并不包含具体的战术建模该如何设计,比如聚合、实体啥的,和领域专家并不需要讨论这部分内容,而是系统所包含的业务,就像“提到我”系统中,我问我自己“我做这个项目的核心业务是什么?”。

领域专家并不是一个职位,他可以是精通业务的任何人。他们可能了解更多的关于业务领域的背景知识,他们可能是软件产品的设计者,甚至有可能是销售员。

实际项目的实践

好,概念说太多没什么意义,实际应用才有价值,我现在在开发一个 JS 权限申请和审核系统,就是我们园子里的 JS 权限申请,因为现在申请 JS 权限需要发邮件进行申请,对于用户申请和我们处理来说,都比较麻烦并且费时间,所以为了解决这个问题,我们想做把 JS 权限申请做成像申请博客一样,园友填写申请内容,然后我们进行后台审核,效率可以提升很多,大概就是这样的一个系统,真实的不能再真实了,毕竟园友和我们都会实际接触并使用,这也是一个相对较小的系统,我们就拿它来开刀,看看 DDD 的这把刀锋不锋利。

对于 JS 权限申请和审核系统来说,领域专家是谁?应该是园友和管理员,毕竟他们在使用这个系统,没有人比他们更了解其中的业务了,所以他们就是这个系统的领域专家,需要强调的是,虽然有时候领域专家是开发人员,但在一开始探讨业务系统的时候,一定不能牵扯到数据库和代码的设计,我们应该忘掉数据库和代码,只是单纯的站在领域专家的角度,去探讨和思考业务系统,那领域专家该如何表述这个系统的业务呢?

下面我大致的表述下:

用户填写 JS 权限申请内容,管理员后台进行审核。

有没有搞错?就这么简单???好像又无言以对,因为关于 JS 权限申请和审核系统,最简单的表述就是这样,但如何提炼出所蕴含的业务呢?接下来需要我们深入的探讨下,作为领域专家身份的我,绘制了一张大致的业务流程图:

435188-20160413151945473-1912413516.png

上面这张图可以进行反复的修改,每个领域专家都可以发表自己的意见和建议,经过最激烈的探讨才会让业务系统更加准确,当业务系统确定好之后,我们就可以从中抽离出核心业务了,上面这张图,哪些是核心业务?哪些又是业务流程呢?我大致圈一下:

435188-20160413161637941-1309117000.png

方框圈的是核心业务,圆形圈的是实体的状态变化,核心业务一般包含在最简单的描述中,比如“提到我”系统中的表述“抽离”和“通知”,还有一种区分方式:判断其是否经常发生变化,对于业务流程来说,一般是不会发生变化的,变化的是核心业务,DDD 的设计应对的就是这个变化,再大致总结下:

  • 核心业务:验证用户信息,可以称为申请状态改为“待审核”的前提条件,主要是判断用户是否符合要求,前面的“验证申请状态”也属于这一类。
  • 实体状态变化:JS 权限申请就是一个实体,它有自己的生命周期,并且对于用户来说,它是唯一存在的,从上面的图中,我们可以看到 JS 权限申请的状态变化,这是领域所关心的,能改变实体的状态,就是业务。

那领域模型关心的是哪些业务?其实就是能影响 JS 权限申请状态变化的条件,暂时不看用户申请的部分,先看下管理员审核的部分,因为是人工审核的,所以这就有人为因素的产生,这部分我们在领域模型设计的时候,就没有办法把控,所以可以把这部分排除在领域模型之外,后面 JS 权限申请状态的改变,也是由人为进行导致的,也就是说,对于领域模型来说,我们没有办法进行控制 JS 权限申请状态的改变,所以后面的状态改变我们可以看作是业务流程或者工作流程,有人可能会问:“开通 JS 权限”和“消息通知用户”,算不算是业务?其实这部分可以算是业务,因为它是状态改变后的一种行为,我们可以使用领域事件实现它。

跟踪变化最实用的方法是领域事件和事件存储。我们为领域专家所关心的所以状态改变都创建单独的事件类型,事件的名字和属性表明发生了什么样的事件。当命令操作执行完后,系统发出这些领域事件。事件的订阅方可以接收发生在模型上的所有事件。在接收到事件后,订阅方将事件保存在事件存储中。

有点越说越乱的感觉,先暂时概括下我们设计领域模型所包含的东西:

  • 聚合根和实体:JS 权限申请,命名为 JsPermissionApply,具有唯一性。
  • 值对象:申请状态,命名为 Status,包含待审核、审核通过、审核不通过等。
  • 领域事件:处理 JsPermissionApply 状态改变后的一些工作(开通 JS 权限和发送消息通知)。
  • 领域服务:UserAuthenticationService,验证用户是否合法,以及验证此用户是否能创建 JsPermissionApply。
  • 实体验证:基本验证由 JsPermissionApply 自身负责,在 JsPermissionApply 的构造函数中处理。
  • 实体行为:管理员的审核处理 Process,领域事件在这里触发。

UserAuthenticationService 所做的工作就是上面图中第一个圈和第一个方框的内容,总的概述就是验证用户是否能创建 JsPermissionApply?我之前考虑用工厂实现,但感觉还是不太妥,因为工厂是为创建复杂实体服务的,内部会有一些复杂的操作,对于一些简单的实体创建,我们直接用实体的构造函数进行创建就行,比如 JsPermissionApply 的创建,既然用工厂实现不合适,那直接将操作放在 JsPermissionApply 中会怎样呢?验证自己能否被创建?想想还是有些别扭,所以还是用 UserAuthenticationService 领域服务实现吧,况且领域服务的定义就是如此。

领域服务表示一个无状态的操作,它用于实现特定于某个领域的任务,当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务。

另外,关于实体、值对象、领域事件、领域服务和仓储接口的实现,最好在不同的项目中,如果再同一个项目中的话,可能会造成循环引用的情况,比如仓储接口引用了实体,领域服务引用了仓储接口,如果实体和领域服务实现在同一个项目,就会出现循环引用的问题。

再来总结下,我们分析系统和设计领域模型的步骤:先和领域专家探讨业务系统,经过反反复复的研究,抽离出业务系统的核心业务,然后用战术建模的方式设计领域模型,最后用代码进行实现。领域模型设计好了之后,下面就开始用代码实现了,在代码实现的时候,最好解决方案中只有领域层、基础设施层和单元测试,并且一开始设计的时候,先编写领域层的代码,然后再编写单元测试,最后进行不断的测试和完善,关于数据的持久化,现在最好不要关注,尽量用 Mock 的方式模拟数据。

我先贴一下 JsPermissionApply 实体的部分代码:

using CNBlogs.Apply.Domain.DomainEvents;
using CNBlogs.Apply.Domain.ValueObjects;
using System;
using Microsoft.Practices.Unity;
using CNBlogs.Apply.Infrastructure.IoC.Contracts;namespace CNBlogs.Apply.Domain
{public class JsPermissionApply : IAggregateRoot{private IEventBus eventBus;public JsPermissionApply(){ }public JsPermissionApply(string reason, int userId, string ip){if (string.IsNullOrEmpty(reason)){throw new ArgumentException("申请内容不能为空");}if (reason.Length > 3000){throw new ArgumentException("申请内容超出最大长度");}if (userId == 0){throw new ArgumentException("用户Id为0");}this.Reason = reason;this.UserId = userId;this.Ip = ip;this.Status = Status.Wait;}public int Id { get; private set; }public string Reason { get; private set; }public int UserId { get; private set; }public Status Status { get; private set; } = Status.Wait;public string Ip { get; private set; }public DateTime ApplyTime { get; private set; } = DateTime.Now;public string ReplyContent { get; private set; }public DateTime? ApprovedTime { get; private set; }public bool IsActive { get; private set; } = true;public void Process(string replyContent, Status status){this.ReplyContent = replyContent;this.Status = status;this.ApprovedTime = DateTime.Now;eventBus = IocContainer.Default.Resolve<IEventBus>();if (this.Status == Status.Pass){eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核通过", RecipientId = this.UserId });}else if (this.Status == Status.Deny){eventBus.Publish(new MessageSentEvent() { Title = "系统通知", Content = "审核不通过", RecipientId = this.UserId });}}}
}

JsPermissionApplyTest 单元测试的代码:

using CNBlogs.Apply.Domain.DomainServices;
using CNBlogs.Apply.Domain.ValueObjects;
using CNBlogs.Apply.Infrastructure.Interfaces;
using CNBlogs.Apply.Infrastructure.IoC.Contracts;
using CNBlogs.Apply.Repository.Interfaces;
using System;
using System.Data.Entity;
using System.Threading.Tasks;
using Xunit;
using Microsoft.Practices.Unity;namespace CNBlogs.Apply.Domain.Tests
{public class JsPermissionApplyTest{private IUserAuthenticationService _userAuthenticationService;private IJsPermissionApplyRepository _jsPermissionApplyRepository;private IUnitOfWork _unitOfWork;public JsPermissionApplyTest(){CNBlogs.Apply.BootStrapper.Startup.Configure();_userAuthenticationService = IocContainer.Default.Resolve<IUserAuthenticationService>();_jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();_unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();}[Fact]public async Task Apply(){var userId = 1;var verfiyResult = await _userAuthenticationService.Verfiy(userId);Console.WriteLine(verfiyResult);Assert.Empty(verfiyResult);var jsPermissionApply = new JsPermissionApply("我要申请JS权限", userId, "");_unitOfWork.RegisterNew(jsPermissionApply);Assert.True(await _unitOfWork.CommitAsync());}[Fact]public async Task ProcessApply(){var userId = 1;var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();Assert.NotNull(jsPermissionApply);jsPermissionApply.Process("审核通过", Status.Pass);_unitOfWork.RegisterDirty(jsPermissionApply);Assert.True(await _unitOfWork.CommitAsync());}}
}

代码我就不分析了,基本上是按照我们上面的设计方案实现的,本来想在仓储层模拟数据的,但时间有限,还是使用的 EF 进行数据的持久化和访问,完整的解决方案目录:

435188-20160414092118520-1940032472.png

开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample

上面提交的代码,只是一开始的实现,有些地方可能没有考虑全面,代码也可能会有些粗糙,但因为不是简简单单的示例 Demo,而是实际项目,所以后面我会不断的进行完善,大家如果有什么意见或建议,欢迎探讨~~~


实现领域驱动设计的方式有很多种,你可以战略设计、你也可以战术设计、你可以直接面向对象编写代码、你也可以和领域专家只画一张白板图、又或者写一篇分析业务系统的文章,但就像埃文斯(Eric Evans)的书名一样《领域驱动设计:软件核心复杂性应对之道》,领域驱动设计的核心目的,就是应对软件业务系统的复杂性,所以,不管哪种实现方式,只要能达到这个目的,那就是好的实现领域驱动设计的方式。

JS 权限申请和审核系统还没完,下面又要加一个 Blog 地址更改申请和审核系统,它们会碰撞什么样的火花呢?拭目以待吧。?

转载于:https://www.cnblogs.com/xishuai/p/how-to-implement-ddd.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/470812.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二、搭建Apache服务器 模板引擎

1. 案例&#xff1a;搭建简单的Apache服务器 var http require(http) var fs require(fs)var server http.createServer()var wwwDir D:\\CWork\\node.js黑马程序员\\study_nodejs\\day02\\code\\wwwserver.on(request, function(req, res) {var url req.urlfs.readFile(…

三、案例:留言板 url.parse()

1. url.parse()的使用 2. 留言板案例 index.html: <!DOCTYPE html> <!-- saved from url(0027)http://192.168.150.76:3000/ --> <html lang"en"><head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8…

一、AJAX学习笔记——原生AJAX (ajax简介、XML简介、ajax优缺点、ajax的使用)

第 1 章&#xff1a;原生 AJAX 1.1 AJAX 简介 AJAX 全称为 Asynchronous JavaScript And XML&#xff0c;就是异步的 JS 和 XML。 通过 AJAX 可以在浏览器中向服务器发送异步请求&#xff0c; 最大的优势&#xff1a;无刷新获取数据。 AJAX 不是新的编程语言&#xff0c;而是…

App安全之网络传输安全

移动端App安全如果按CS结构来划分的话&#xff0c;主要涉及客户端本身数据安全&#xff0c;Client到Server网络传输的安全&#xff0c;客户端本身安全又包括代码安全和数据存储安全。所以当我们谈论App安全问题的时候一般来说在以下三类范畴当中。 App代码安全&#xff0c;包括…

二、nodemon-Node.js 监控工具

nodemon-Node.js 监控工具 https://www.npmjs.com/package/nodemon 这个工具在我们改变了服务端代码时&#xff0c;会自动重启服务器&#xff0c;不需要我们再手动去重启服务器了&#xff0c;方面我们后面调试代码&#xff01; 1. 安装 node &#xff1a;http://nodejs.cn/d…

利用动态规划(DP)解决 Coin Change 问题

问题来源 这是Hackerrank上的一个比较有意思的问题&#xff0c;详见下面的链接&#xff1a; https://www.hackerrank.com/challenges/ctci-coin-change 问题简述 给定m个不同面额的硬币&#xff0c;C{c0, c1, c2…cm-1}&#xff0c;找到共有几种不同的组合可以使得数额为n的…

jquery datatable设置垂直滚动后,表头(th)错位问题

jquery datatable设置垂直滚动后&#xff0c;表头(th)错位问题 问题描述&#xff1a; 我在datatable里设置&#xff1a;”scrollY”: ‘300px’,垂直滚动属性后&#xff0c;表头的宽度就会错位&#xff0c;代码如下&#xff1a; <!-- HTML代码 --> <table id"dem…

三、解决ie缓存问题

解决 IE 缓存问题 问题&#xff1a;在一些浏览器中(IE),由于缓存机制的存在&#xff0c;ajax 只会发送的第一次请求&#xff0c;剩余多次请求不会在发送给浏览器而是直接加载缓存中的数据。 在谷歌浏览器中&#xff0c;修改了服务器代码&#xff0c;重新发送请求时&#xff0…

imageNamed和imageWithContentsOfFile-无法加载图片的问题

问题描述 图片资源放在Assets.xcassets中&#xff0c;分别用UIImage的类方法imageNamed和imageWithContentsOfFile获取图片对象&#xff0c;但发生奇怪的情况&#xff0c;前者获取到图片对象&#xff0c;后者结果为nil。代码如下&#xff1a; 1.通过UIImage的类方法imageNamed:…

LeetCode 309: 一个很清晰的DP解题思路

问题来源 题目来源链接见下方&#xff1a; https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/ 问题简述&#xff1a; 假如有一个 i 个元素的数组&#xff0c;数组的每个元素表示了第 i 天某只股票的价格&#xff0c;设计一种算法来…

五、手动取消ajax请求 解决重复发送请求问题

server.js: // 1. 引入express const express require(express)// 2. 创建应用对象 const app express()// 3. 创建路由规则 app.get(/server, (request, response) > {// 设置响应头 设置允许跨域response.setHeader(Access-Control-Allow-Origin, *)// 设置响应体respo…

linux ps命令详解

ps命令用于监测进程的工作情况。进程是正在运行的程序&#xff0c;一直处于动态变化中&#xff0c;而ps命令所显示的进程工作状态时瞬间的。 使用方式&#xff1a;ps[options][-help] 常用参数&#xff1a; -A &#xff1a;显示所有进程 -a&#xff1a;显示一个终端的所有进程。…

用多元线性回归预测网页访问量(R语言)

前言 该问题来源于《机器学习:实用案例解析》中的第5章。在书中&#xff0c;已经对该问题给出了一种解决方案&#xff0c;但是我觉得写的还是太简略了一些&#xff0c;没有把考虑问题的整个思路给写出来&#xff0c;所以&#xff0c;在这里给出我的一些想法。 问题简述 我们…

六、jQuery 中的 AJAX 跨域问题

第 2 章&#xff1a;jQuery 中的 AJAX 官方中文文档&#xff1a;https://jquery.cuishifeng.cn/jQuery.Ajax.html 2.1 get 请求 $.get(url, [data], [callback], [type]) url:请求的 URL 地址。data:请求携带的参数。callback:载入成功时回调函数。type:设置返回内容格式&a…

常用的几种编程语言的介绍

编程语言&#xff08;programming language&#xff09;&#xff0c;来自百度百科的解释为&#xff1a;编程语言是用来定义计算机程序的形式语言。它是一种被标准化的交流技巧&#xff0c;用来向计算机发出指令。一种计算机语言让程序员能够准确地定义计算机所需要使用的数据&a…

三门问题(Monty Hall problem)背后的贝叶斯理论

文章目录1 前言2 问题简介3 直观的解释4 贝叶斯理论的解释1 前言 三门问题可以说有着各种版本的解释&#xff0c;但我看了几个版本&#xff0c;觉得没有把其中的条件说清楚&#xff0c;所以还是决定按照自己的理解记录一下这个特别有意思的问题。 2 问题简介 三门问题&#…

一、在vue项目中使用mock.js(详解)

步骤1.搭建测试项目 步骤1.1创建项目 命令&#xff1a; vue create mock-demo 步骤1.2安装依赖 命令&#xff1a; #使用axios发送ajax cnpm install axios--save #使用mockjs产生随机数据 cnpm install mockjs--save-dev #使用json5解决ison文件&#xff0c;无法添加注释…

二、在jQuery中使用mockjs

在jQuery项目中使用mock.js 步骤1.搭建项目 步骤1.1创建项目 新建文件夹jquery-mock-demo 步骤1.2新建index.html&#xff0c;引入jquery.js文件和mock.js <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title&g…

Android 中如何计算 App 的启动时间?

&#xff08;转载&#xff09; 已知的两种方法貌似可以获取&#xff0c;但是感觉结果不准确&#xff1a;一种是&#xff0c;adb shell am start -w packagename/activity,这个可以得到两个值&#xff0c;ThisTime和TotalTime&#xff0c;不知道两个有什么区别&#xff0c;而且与…