流花飘原

流花飘原 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

流花飘原 关注了专栏 · 2020-08-30

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 58557

流花飘原 回答了问题 · 2020-08-10

请问next.js在getStaticProps里怎样获取路由参数

你要注意,next是分为纯静态渲染和SSR服务端渲染
你使用getStaticProps是开启纯静态渲染,是无法获得动态的路由参数进行解析,除非你使用了动态路径解析。
像你的需求你,需要将数据获取放到getServerSideProps中,而不要使用getStaticProps
这样你可以从中获得context,进而取得路径参数:


 export async function getServerSideProps(context) {
   console.log(context)
   //here,you can fetch data by context.query
   return {
     props: {
      query:context.query    
      }, // will be passed to the page component as props
   }
 }

关注 2 回答 1

流花飘原 关注了用户 · 2020-04-23

Java知识分享 @javajishufenxiang

vx:k2656806354 Java技术分享

关注 5

流花飘原 赞了文章 · 2020-04-23

为啥国人喜欢Mybatis,而老外偏爱 Hibernate/JPA 呢?

这是一篇很不错的文章,长见识,文章有点长,大家可以慢慢读。会有所收获的。

关于SQL 和 ORM 的争论,永远都不会终止,我也一直在思考这个问题。昨天又跟群里的小伙伴进行了一番讨论,感触还是有一些,于是就有了今天这篇文。

声明:

本文不会下关于 Mybatis 和 JPA 两个持久层框架哪个更好这样的结论。只是摆事实,讲道理,所以,请各位看官勿喷。

一、事件起因

关于 Mybatis 和 JPA 孰优孰劣的问题,争论已经很多年了。一直也没有结论,毕竟每个人的喜好和习惯是大不相同的。

我也看过知乎上一些问答,各有各的理由,感觉都挺有道理。如果让我不带感情色彩地去分辨,其实我也是懵的,因为真的是公说公有理婆说婆有理。

而在国内,不得不承认,用 Mybatis 的公司确实是要比用 JPA 的多,但是在 2015 年以前,用 Hibernate 的公司确实也是很多的。

为什么在国内,会有这样的现象发生?而在国外,老外会一如既往地使用 JPA 呢?我们来分析分析。

二、目前生态

在最近(2018)的 JVM 生态报告中(https://snyk.io/blog/jvm-ecos...),Mybatis是使用率是很低的。可以看图:

可以看出,Mybatis 的占比只有可怜的 6%,大家看到这个统计结果应该会很吃惊,你会觉得,不对啊,我公司以及我很多朋友都在用 Mybatis 啊,好像没听说过有人用 JPA 的,这个统计结果是错的吧?

再从下面这个对比来看,MyBatis 的关注主要集中在中日韩。知道日韩为啥也高吗,猜中有奖哦,哈哈!

首先,必须指出,对于青年程序员,其实都会质疑这个图的可信度。

中老年程序员都在感叹国外其实更注重开发效率和面向对象的分析和设计。但我可以非常负责任地告诉你,这图是真的。那么,造成这种现象的原因是?

三、国人喜欢 Mybatis 的原因

总结起来,有如下原因:

1.大厂带节奏

国内做互联网的 Java 程序很多都是拷贝阿里的,阿里一开始用例 iBatis,大量的老系统都是基于 iBatis/MyBatis 的,市场上对 MyBatis 熟悉的人才更多,招聘和培训更容易,有的青年程序员以为“MyBatis 早已统一全球了”就是一个很好的证明。

2.简单,学习成本低

小公司需要大量入门级的程序员,像大神甚至一个都请不起,请问大神们那些牛 b 框架哪个更快让菜鸟们上手,降低公司学习成本。注意这个成本会一直跟随公司,想必大神们创业直接前后端分离了,毕竟钱嘛多的是。

3.对于复杂性需求的灵活性高

国内绝大部分项目都是面向表结构编程的,把 java 对象仅当成数据容器,查询和模型变更都设计在一张表上,所谓业务逻辑就是一堆增删改查的 sql 集合,当然用 mybatis 方便。

在逻辑不复杂,或者你判断软件生命周期不会超过一年的时候,直接用表结构编程是最方便快捷的。

国内普遍都是分布式,流量和性能决定了需要经常进行优化,而是用 Mybatis 对复杂需求的优化很方便。

4.公司环境

国内好多项目都是应付领导的某些奇葩需求。需要面向领导编程。一大半时间其实都是在解决领导的需求。

国内项目需要大量报表统计,需要提供给领导作为决策。看到这里,各位领导不要骂我 ,真的不是黑领导的。

5.Hibernate学习成本高

虽然,实际上 SpringDataJPA 是非常简单的,但是,但是,JPA/Hibernate 后期调试跟踪问题很麻烦,改起来也麻烦。别忘了,牛逼如你的人全公司甚至一个都没。

还有什么缓存什么 Criteria 什么 Lazy,虽然这些你学了也不见得能用上,但一个框架,你不学还是不行的。

推荐阅读:JPA、Hibernate、Spring Data JPA 的关系

而且,JPA 对于增删改很方便,复杂查询却是软肋,有同学会说,JPA 也能写 SQL 语句啊,我想说的是,既然都用 orm 了,你再写 sql,那不就失去了 oop 的内涵了吗?不优雅好吧。

四、老外喜欢 JPA 的原因

1.很多老外对 Mybatis 的认知还停留在 iBatis 阶段

实际上在 Mybatis 的应用场景里面,开发者要的就是自动封装,把 sql 查询结果转化为指定的 java 对象。

这个在 iBatis 阶段,需要开发者自己定义大量的 xml 配置,去指定数据库表字段与 Java 实体类之间的关系。并且,对于每一条 sql,都需要在 xml 中写相应的语句,虽然有代码生成器,带开发量还是不小的。

但 Mybatis 发展到今天,已经非常完美地做好了自动封装数据对象这件事,支持的插件也比较丰富。对于常见的增删改查,也不需要自己写一行代码,这已经无限接近于 Hibernate 的能力了。

推荐阅读:为什么老外都不愿意用MyBatis?

2.喜欢 OOP、DDD

认为写 SQL 不优雅,用 jpa 的核心是让我们关注对象建模,而不是关心底层数据库映射。只有你在考虑数据和行为在一起的充血模型、贴身职责,聚合根状态变迁,值对象不变性的情况下,你才会发现 jpa 给你提供了很多便利,根本不需要关注底层存储模型。

在复杂的逻辑、超长的软件生命周期。使用 DDD 的设计方法是目前看比较合理的选择,维护的成本比较低。

DDD 全称是(Domain-Driven Design)这是 2004 年就出来的理论,复杂逻辑的应对之道。DDD 大会在欧洲等地办了一届又一届,CQRS、Event Sourcing 等探索层出不穷,这也是为什么国外比较流行 JPA 原因。

不过,国内主要是随着这两年随着微服务火爆也有人谈起来 DDD 了。但其实 DDD 也不是银弹,需要大拿能把控全局,国内缺的就是这种大拿,搬砖的太多。

3.有些老外在技术选型时,不会考虑除 Spring 这种知名框架外的其他技术,无他,唯手熟尔。

Spring确实很强,我也写了 N 篇最新 Spring 教程,关注微信公众号:Java技术栈,在后台回复:spring,可以获取,都是干货。

国外一个项目,做了几年十几年都是很正常的。我以前接触过一个国外的的电商项目,做了十几年,也跑的好好的,这就是证据。

使用技术也是有惯性的。

4.数据体量和种类没有达到

个人感觉,也咨询了国际友人。老外的项目,在数据体量和种类上完全达不到国内的水平。

所以,他们对于性能上的渴求度没有那么高。追求的是稳定,可维护性好。国内一个双 11,如果用 hibernate,那只能死掉了。

推荐阅读:淘宝千万并发,14 次架构演进…

也说明,老外的需求主要是在业务上,技术层面较少考虑。

五、一点感悟

整个状况,和对 OOAD 的重视有很大关系,我在做 DDD 技术落地的时候,用 MyBatis 非常蹩脚,用 JPA/Hibernate 会好很多。

JPA/Hibernate 比较复杂,团队中要有人 Hold 住它,否则及其容易踩坑;另外,真要使用,建议使用它的一个功能子集,不要所有功能都用。也可以尝试使用更简单 EBean ORM。

JPA/Hibernate 对分库分表的支持有一下坑。虽然,使用 Shareding-JDBC 或 MyCat 等技术,可以不关心分库分表,但是,JPA/Hibernate 在某些情况下(比如加载子集合的时候)可能会不带分区键。国外分库分表的少,国内几乎是标配。

六、Mybatis 和 JPA 大比较

最后,从多维度对这两个知名框架做些比较:

最后的最后,欢迎在评论区留下你最宝贵的意见 ,勿喷哦!

最后的最后,关于这个话题,你怎么看待的?欢迎在底部留言!

参考:zhihu.com/question/50729231/answer/549761974
来源:blog.csdn.net/m0_37609579/article/details/102961765

推荐去我的博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

觉得不错,别忘了点赞+转发哦!

查看原文

赞 4 收藏 0 评论 2

流花飘原 赞了文章 · 2020-04-22

六个好用的前端开发在线工具

[Mahdhi Rezvi] 原作,翻译转载自 New Frontend (缩进段落为译者附注)。

网上可以找到前端开发社区贡献的大量工具,这篇文章列出了我最喜欢的一些工具,这些工具给我的工作带来了许多便利。

1. EnjoyCSS

老实说,虽然我做过许多前端开发,但我并不擅长 CSS。当我陷入困境时,[EnjoyCSS] 是我的大救星。EnjoyCSS 提供了一个简单的交互界面,帮助我设计元素,然后自动输出相应的 CSS 代码。

EnjoyCSS 首页

EnjoyCSS 可以输出 CSS、LESS、SCSS 代码,并支持指定需要支持哪些浏览器及其最低版本。开发简单页面时用起来比较方便,但不太适合复杂一点的前端项目(这类项目往往需要引入 CSS 框架)。

2. Prettier Playground

[Prettier] 是一个代码格式化工具,支持格式化 JavaScript 代码(包括 [ES2017]、[JSX]、[Angular]、[Vue]、[Flow]、[TypeScript] 等)。Prettier 会移除代码原本的样式,替换为遵循最佳实践的标准化、一致的样式。IDE 大多支持 Prettier 工具,不过 Prettier 也有在线版本,让你可以在浏览器里格式化代码。

Prettier Playground 分左右两栏,左边是原始代码,右边是格式化后的代码

如果工作电脑不在手边,使用移动端设备或者临时借用别人的电脑查看代码时,Prettier Playground 非常好用。相比在 IDE 或编辑器下使用 Prettier,个人更推荐通过 git pre-commit hook 配置 Prettier:hook 可以保证整个团队使用统一的配置,免去各自分别配置 IDE 或编辑器的麻烦。如果是老项目,hook 还可以设置只格式化有改动的单个文件甚至有改动的代码段,避免在 IDE 或编辑器下使用 Prettier 时不小心格式了大量代码,淹没了 commit 的主要改动,让 review 代码变得十分痛苦。

3. Postman

[Postman] 一直在我的开发工具箱里,测试后端 API 接口时非常好用。GET、POST、DELETE、OPTIONS、PUT 这些方法都支持。毫无疑问,你应该使用这个工具。

Postman 界面

Postman 之外,[Insomnia] 也是很流行的 REST API 测试工具,亮点是支持 [GraphQL]。不过 Postman 从 去年夏天发布的 v7.2 起也支持了 GraphQL。

4. StackBlitz

[Chidume Nnamdi] 盛赞这是每个用户最喜欢的在线 IDE。[StackBlitz] 将大家最喜欢、最常用的 IDE Visual Studio Code 搬进了浏览器。

StackBlitz 支持一键配置 Angular、[React]、Ionic、TypeScript、RxJS、[Svelte] 等 JavaScript 框架,也就是说,只需几秒你就可以开始写代码了。

我觉得这个在线 IDE 很有用,特别是可以在线尝试一些样例代码或者库,否则仅仅尝试一些新特性就需要花很多时间在新项目初始化配置上。有了 StackBlitz,无需在本地从头搭建环境,花上几分钟就可以试用一个 NPM 包。很棒,不是吗?

StackBlitz 首页

微软官方其实也提供了在线版本的 VSCode,可以在浏览器内使用 VSCode,并且支持开发 Node.js 项目(基于 Azure)。不过 StackBlitz 更专注于优化前端开发体验,界面更加直观一点,也推出了 beta 版本的 Node.js 支持(基于 GCP,需要填表申请)。

5. Bit.dev

软件开发的基本原则之一就是代码复用。代码复用减少了开发量,让你不用从头开发组件。

这正是 [Bit.dev] 做的事,分享可重用的组件和片段,降低开发量,加速开发进程。

除了公开分享,它还支持在团队分享,让团队协作更方便。

正如 Bit.dev 的口号「组件即设计体系。协同开发更好的组件。」所言,Bit.dev 可以用来创建设计体系,允许团队内的开发者和设计师一起协作,从头搭建一套设计体系。

Bit.dev 目前支持 [React]、Vue、Angular、Node 及其他 JavaScript 框架。

在 Bit.dev 上搜索时间选择器组件

在 Bit.dev 上不仅可以搜索组件,还可以直接查看组件的依赖,浏览组件的代码,甚至在线编辑代码并查看预览效果!选好组件后可以通过 Bit.dev 的命令行工具 bit 在本地项目引入组件,也可以通过 npm、yarn 引入组件。

6. CanIUse

[CanIUse]是非常好用的在线工具,可以方便地查看各大浏览器对某个特性的支持程度。

我过去经常碰到自己开发的应用的一些功能在其他浏览器下不支持的情况。比如我的作品集项目使用的某个特性在 Safari 下不支持,直到项目上线几个月后我才意识到。这些经验教训让我意识到需要检查浏览器兼容性。

我们来看一个例子吧。哪些浏览器支持 WebP 图像格式?

通过 CanIUse 查看 WebP 的兼容性

如你所见,Safari 和 IE 目前不支持 WebP。这意味着需要为不兼容的浏览器提供回退选项,比如:

<picture>
    <source srcset="img/awesomeWebPImage.webp" type="image/webp">
    <source srcset="img/creakyOldJPEG.jpg" type="image/jpeg">
    <img data-original="img/creakyOldJPEG.jpg" alt="Alt Text!">
</picture>
CanIUse 还可以在命令行下使用,例如,在命令行下查看 WebP 图像格式的浏览器兼容性:caniuse webp(运行命令前需要事先通过 npm install -g caniuse-cmd 安装命令行工具。

codecaniuse webp/code 的命令行输出结果

查看原文

赞 45 收藏 25 评论 0

流花飘原 赞了文章 · 2020-04-22

Gitee上 9.6K Star的王牌项目,这个Java工具类库你不能错过

Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。

项目名称:hutool

项目作者:hutool

开源许可协议:MulanPSL-2.0

Hutool 中的工具方法来自于每个用户的精雕细琢,它涵盖了 Java 开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;

Hutool 是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的 Bug。

Hutool 的目标是使用一个工具方法代替一段复杂代码,从而最大限度的避免“复制粘贴”代码的问题,彻底改变我们写代码的方式。

以计算 MD5 为例:

  • 【以前】打开搜索引擎 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用
  • 【现在】引入Hutool -> SecureUtil.md5()

Hutool 的存在就是为了减少代码搜索成本,避免网络上参差不齐的代码出现导致的 Bug。

包含组件

一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:

可以根据需求对每个模块单独引入,也可以通过引入hutool-all方式引入所有模块。

如果你觉得这个项目不错,欢迎去项目主页给它一个 Star:https://gitee.com/loolly/hutool

查看原文

赞 3 收藏 2 评论 0

流花飘原 赞了文章 · 2020-04-11

[Skr-Shop]购物车之架构设计

来还债了,希望大家在疫情中都是平安的,回来的时候公司也还在!


skr shop是一群底层码农,由于被工作中的项目折磨的精神失常,加之由于程序员的自傲:别人设计的系统都是一坨shit,我的设计才是宇宙最牛逼,于是乎决定要做一个只设计不编码的电商设计手册。

在上一篇文章 购物车设计之需求分析 描述了购物车的通用需求。本文重点则在如何实现上进行架构上的设计(业务+系统架构)。

说明

架构设计可以分为三个层面:

  • 业务架构
  • 系统架构
  • 技术架构

快速简单的说明下三个架构的意思;当我们拿到购物车需求时,我们说用Golang来实现,存储用Redis;这描述的是技术架构;我们对购物车代码项目进行代码分层,设计规范,以及依赖系统的规划这叫系统架构;

那业务架构是什么呢?业务架构本质上是对系统架构的文字语言描述;什么意思?我们拿到一个需求首先要跟需求方进行沟通,建立统一的认知。比如:规范名词(购物车中说的商品与商品系统中商品的含义是不同的);建立大家都能明白的模型,购物车、用户、商品、订单这些实体之间的互动,以及各自具备什么功能。

在业务架构分析上有很多方法论,比如:领域驱动设计,但是它并不是唯一的业务架构分析方法,也并不是说最好的。适合你的就是最好的。我们常用的实体关系图、UML图也属于业务架构领域;

这里需要强点一点的是,不管你用什么方式来建模设计,有设计总比没设计强,其次一定要将建模的内容体现到你的代码中去。

本文在业务架构上的分析借助了 DDD (领域驱动设计)思想;还是那句话适合的就是最好的

业务架构

通过前面的需求分析,我们已经明确我们的购物车要干什么了。先来看一下一个典型的用户操作购物车过程。

用户旅程

在这个过程中,用户使用购物车这个载体完成了商品的购买流程;不断流动的数据是商品,购物车这个载体是稳定的。这是我们系统中的稳定点与变化点。

商品的流动方式可能多种多样,比如从不同地方加入购物车,不同方式加入购物车,生命周期在购物车中也不一样;但是这个流程是稳定的,一定是先让购物车中存在商品,然后才能去结算产生订单。

商品在购物车中的生命周期如下:

过程

按照这个过程,我们来看一下每个阶段对应的操作。

过程对应的操作

这里注意一点,加车前这个操作其实我们可以放到购物车的添加操作中,但是由于这部分是非常不稳定且多变的。我们将其独立出来,方便后续进行扩展而不影响相对比较稳定的购物车阶段。

上面这三个阶段,按照DDD中的概念,应该叫做实体,他们整体构成了购物车这个域;今天我们先不讲这些概念,就先略过,后面有机会单独发文讲解。

加车前

通过流程分析,我们总结出了系统需要具备的操作接口,以及这些接口对应的实体,现在我们先来看加车前主要要做些什么;

加车前其实主要就是对准备加入的购物车商品进行各个纬度的校验,检查是否满足要求。

在让用户加车前,我们首先解决的是用户从哪里卖,然后进行验证?因为同一个商品从不同渠道购买是存在不同情况的,比如:小米手机,我们是通过秒杀买,还是通过好友众筹买,或者商城直接购买,价格存在差异,但是实际上他是同一个商品;

第二个问题是是否具备购买资格,还是上面说的,秒杀、众筹这个加车操作,不是谁都可以添加的,得现有资格。那么资格的检查也是放到这里;

第三个问题是对这个购买的商品进行商品属性上的验证,如是否上下架,有库存,限购数量等等。

而且大家会发现,这里的验证条件可能是非常多变的。如何构建一个方便扩展的代码呢?

加车的验证

整个加车过程,重要的就是根据来源来区分不同的验证。我们有两种选择方式。

方式一:通过策略模式+门面模式的方式来搞定。策略就是根据不同的加车来源进行不同的验证,门面就是根据不同的来源封装一个个策略;

方式二:通过责任链模式,但是这里需要有一个变化,这个链在执行过程中,可以选择跳过某些节点,比如:秒杀不需要库存、也不需要众筹的验证;

通过综合的分析我选择了责任链的模式。贴一下核心代码

// 每个验证逻辑要实现的接口
type Handler interface {
    Skipped(in interface{}) bool // 这里判断是否跳过
    HandleRequest(in interface{}) error // 这里进行各种验证
}

// 责任链的节点
type RequestChain struct {
    Handler
    Next *RequestChain
}

// 设置handler
func (h *RequestChain) SetNextHandler(in *RequestChain) *RequestChain {
    h.Next = in
    return in
}

关于设计模式,大家可以看我小伙伴的github:https://github.com/TIGERB/eas...

购物车

说完了加车前,现在来看购物车这一部分。我们在之前曾讨论过,购物车可能会有多种形态的,比如:存储多个商品一起结算,某个商品立即结算等。因此购物车一定会根据渠道来进行购物车类型的选择。

这部分的操作相对是比较稳定的。我们挑几个比较重要的操作来讲一下思路即可。

加入购物车

通过把条件验证的前置,会发现在进行加车操作时,这部分逻辑已经变得非常的轻量了。要做的主要是下面几个部分的逻辑。

加入购物车

这里有几个取巧的地方,首先是获取商品的逻辑,由于在前面验证的时候也会用到,因此这里前面获取后会通过参数的方式继续往后传递,因此这里不需要在读库或者调用服务来获取;

其次这里需要把当前用户现有购物车数据获取到,然后将添加的这个商品添加进来。这是一个类似合并操作,原来这个商品是存在,相当于数量加一;需要注意这个商品跟现存的商品有没有父子关系,有没有可能加入后改变了某个活动规则,比如:原来买了2个送1个赠品,现在再添加了一个变成3个,送2个赠品;

注意:这里的添加并不是在购物车直接改数量,可能就是在列表、详情页直接添加添加。

通过将合并后的购物车数据,通过营销活动检查确认ok后,直接回写到存储中。

合并购物车

为什么会有合并购物车这个操作?因为一般电商都是准许游客身份进行操作的,因此当用户登录后需要将二者进行合并。

这里的合并很多部分的逻辑是可以与加入购物车复用的逻辑。比如:合并后的数据都需要检查是否合法,然后覆写回存储中。因此大家可以看到这里的关联性。设计的方法在某种程度上要通用。

购物车列表

购物车列表这是一个非常重要的接口,原则上购物车接口会提供两种类型,一种简版,一种完全版本;

简版的列表接口主要是用在类似PC首页右上角之类获取简单信息;完全版本就是在购物车列表中会用到。

在实际实现中,购物车绝不仅仅是一个读取接口那么简单。因为我们都知道不管是商品信息、活动信息都是在不断的发生变化。因此每次的读取接口必然需要检查当前购物车中数据的合法性,然后发现不一致后需要覆写原存储的数据。

购物车列表

也有一些做法会在每个接口都去检查数据的合法性,我建议为了性能考虑,部分接口可以适当放宽检查,在获取列表时再进行完整的检查。比如添加接口,我只会检测我添加的商品的合法性,绝不会对整个购物车进行检查。因为该操作之后一般都会调用列表操作,那么此时还会进行校验,二者重复操作,因此只取后者。

结算

结算包括两部分,结算页的详情信息与提交订单。结算页可以说是在购物车列表上的一个包装,因为结算页与列表页最大的不同是需要用户选择配送地址(虚拟商品另说),此时会产生更明确的价格信息,其他基本一致。因此在设计购物车列表接口的时候,一定要考虑充分的通用性。

这里另外一个需要注意的是:立即购买,我们也会通过结算页接口来实现,但是内部其实还是会调用添加接口,将商品添加到购物车中;有三个需要注意的地方,首先是这个添加操作是服务内部完成的,对于服务调用方是不需要感知这个加入操作的存在;其次是这个购物车在Redis中的Key是独立于普通购物车的,否则二者的商品耦合在一起非常难于操作处理;最后立即购买的购物车要考虑账号多终端登录的时候,彼此数据不能互相影响,这里可以用每个端的uuid来作为购物车的标记避免这种情况。

购物车的最后一步是生成订单,这一步最要紧的是需要给购物车加锁,避免提交过程中数据被篡改,多说一句,很多人写的Redis分布式锁代码都存在缺陷,大家一定要注意原子性的问题,这类文章网络上很多不再赘述。

加锁成功之后,我们这里有多种做法,一种是按照DB涉及组织数据开始写表,这适用于业务量要求不大,比如订单每秒下单量不超过2000K的;那如果你的系统并发要求非常高怎么办?

其实也很简单,高性能的三大法宝之一:异步;我们提交的时候直接将数据快照写入MQ中,然后通过异步的方式进行消费处理,可以通过通过控制消费者的数量来提升处理能力。这种方法虽然性能提升,但是复杂度也会上升,大家需要根据自己的实际情况来选择。

关于业务架构的设计,到此告一段落,接下来我们来看系统架构。

系统架构

系统结构主要包含,如何将业务架构映射过来,以及输出对应输入参数、输出参数的说明。由于输入、输出针对各自业务来确定的,而且没有什么难度,我们这里就只说如何将业务架构映射到系统架构,以及系统架构中最核心的Redis数据结构选择以及存储的数据结构设计。

代码结构

下面的代码目录是按照 Golang 来进行设计的。我们来看看如何将上面的业务架构映射到代码层面来。

├── addproducts.go
├── cartlist.go
├── mergecart.go
├── entity
│   ├── cart
│   │   ├── add.go
│   │   ├── cart.go
│   │   └── list.go
│   ├── order
│   │   ├── checkout.go
│   │   ├── order.go
│   │   └── submit.go
│   └── precart
├── event
│   └── sendorder.go
├── facade
│   ├── activity.go
│   └── product.go
└── repo

外层有 entityeventfacaderepo这四个目录,职责如下:

entity: 存放的是我们前面分析的购物领域的三个实体;所有主要的操作都在这三个实体上;

event: 这是用来处理产生的事件,比如刚刚说的如果我们提交订单采用异步的方式,那么该目录就该完成的是如何把数据发送到MQ中去;

facade: 这儿目录是干嘛的呢?这主要是因为我们的服务还需要依赖像商品、营销活动这些服务,那么我们不应该在实体中直接调用它,因为第三方可能存在变动,或者有增加、减少,我们在这里进行以下简单的封装(设计模式中的门面模式);

repo: 这个目录从某种程度上可以理解为 Model层,在整个领域服务中,如果与持久化打交道,都通过它来完成。

最后外层的几个文件,就是我们所提供的领域服务,供应用层来进行调用的。

为了保证内容的紧凑,我这里放弃了对整个微服务的目录介绍,只单独介绍了领域服务,后续会单独成文介绍下微服务的整个系统架构。

通过上面的划分,我们完成了两件事情:

  1. 业务架构分析的结构在系统代码中都有映射,他们彼此体现。这样最大的好处是,保证设计与代码的一致性,看了文档你就知道对应的代码在哪里;
  2. 每个目录各自的关注点都进行了分离,更内聚,更容易开发与维护。

Redis存储

现在来看,我们选择Redis作为购物商品数据的存储,我们要解决两个问题,一是我们需要存哪些数据?二是我们用什么结构来存?

网络上很多写购物车的都是只保存一个商品id,真实场景是很难满足需求的。你想想,一个商品id如何记住用户选择的赠品?用户上次选择的活动?以及购买的商品渠道?

综合比较通用的场景,我给出一个参考结构:

// 购物车数据
type ShoppingData struct {
    Item       []*Item `json:"item"`
    UpdateTime int64   `json:"update_time"`
    Version    int32   `json:"version"`
}

// 单个商品item元素
type Item struct {
    ItemId       string          `json:"item_id"`
    ParentItemId string          `json:"parent_item_id,omitempty"` // 绑定的父item id
    OrderId      string          `json:"order_id,omitempty"`       // 绑定的订单号
    Sku          int64           `json:"sku"`
    Spu          int64           `json:"spu"`
    Channel      string          `json:"channel"`
    Num          int32           `json:"num"`
    Status       int32           `json:"status"`
    TTL          int32           `json:"ttl"`                     // 有效时间
    SalePrice    float64         `json:"sale_price"`              // 记录加车时候的销售价格
    SpecialPrice float64         `json:"special_price,omitempty"` // 指定价格加购物车
    PostFree     bool            `json:"post_free,omitempty"`     // 是否免邮
    Activities   []*ItemActivity `json:"activities,omitempty"`    // 参加的活动记录
    AddTime      int64           `json:"add_time"`
    UpdateTime   int64           `json:"update_time"`
}

// 活动
type ItemActivity struct {
    ActID    string `json:"act_id"`
    ActType  string `json:"act_type"`
    ActTitle string `json:"act_title"`
}

重点说一下 Item 这个结构,item_id 这个字段是标记购物车中某个商品的唯一标记,因为我们之前说过,同一个sku由于渠道不同,那么在购物车中会是两个不同的item;接下来的 parent_item_id 字段是用来标记父子关系的,这里将可能存在的树结构转成了顺序结构,我们不管是父商品还是子商品,都采用顺序存储,然后通过这个字段来进行关联;有些同学可能会奇怪,为什么会存order id这个字段呢?大家关注下自己的日常业务,比如:再来一单、定金预售等,这种一定是与某个订单相关联的,不管是为了资格验证还是数据统计。剩下的字段都是一些非常常规的字段,就不在一一介绍了;

字段的类型,大家根据自己的需要进行修改。

接下来该说怎么选择Redis的存储结构了,Redis常用的 Hash Table、集合、有序集合、链表、字符串 五种,我们一个个来分析。

首先购车一定有一个key来标记这个购物车属于哪个用户的,为了简化,我们的key假设是:uid:cart_type

我们先来看如果用 Hash Table;我们添加时,需要用到如下命令:HSET uid:cart_type sku ShoppingData;看起来没问题,我们可以根据sku快速定位某个商品然后进行相关的修改等,但是注意,ShoppingData是一个json串,如果用户购物车中有非常多的商品,我们用 HGETALL uid:cart_type 获取到的时间复杂度是O(n),然后代码中还需要一一反序列化,又是O(n)的复杂度。

如果用集合,也会遇到类似的问题,每个购物车看做一个集合,集合中的每个元素是 ShoppingData ,取到代码中依然需要逐一反序列化(反序列化是成本),关于有序集合与链表就不在分析,大家可以按照上面的思路去尝试下问题所在。

看起来我们没得选,只有使用String,那我们来看一下String的契合度是什么样子。首先SET uid:cart_type ShoppingDataArr;我们把购物车所有的数据序列化成一个字符串存储,每次取出来的时间复杂度是O(1),序列化、反序列化都只需要一次。看来是非常不错的选择。但是在使用中大家还是有几点需要注意。

  1. 单个Value不能太大,要不然就会出现大key问题,所以一般购物车有上限限制,比如item不能超过多少个;
  2. 对redis的操作性能提升上来了,但是代码的就是修改单个item时的不便,必须每次读取全部然后找到对应的item进行修改;这里我们可以把从redis中的数据读取出来后,在内存中构建一个HashTable,来减少每次遍历的复杂度;

网上也看到很多Redis数据结构组合使用来保存购物车数据的,但是无疑增加了网络开销,相比起来还是String最经济划算。

总结

至此对于购物车的实现设计算是完结了,其中关于订单表的设计会单独放到订单模块去讲。

对于整个购物车服务,虽然没有写的详细到某个具体的接口,但是分析到这一步,我相信大家心中都是有沟壑的,能够结合自己的业务去实现它。

文中有些很有意思的地方,建议大家动手去做做看,有任何问题,我们随时交流。

  • 改编版的责任链模式
  • Redis的分布式事务锁实现

接下来终于要到订单部分的设计了,希望大家继续关注我们。

项目地址:https://github.com/skr-shop/m...

查看原文

赞 31 收藏 19 评论 5

流花飘原 关注了专栏 · 2020-03-15

领域驱动设计DDD与微服务实战案例进阶

讨论领域驱动设计、微服务、分布式架构的思想、方法和后端编码实现。 DDD与微服务实战视频课程请关注公众号:msshcj

关注 66

流花飘原 赞了文章 · 2020-03-15

领域驱动设计实战案例(一):搭建支持DDD的轻量级框架

DDD实战进阶第一波:开发一般业务的大健康行业直销系统(概述)

近年来,关于如何开发基于业务的软件系统与产品一直是软件行业的一个重要内容。对于架构师与软件开发人员来说,开发此类系统头痛的问题大概是以下几个方面:

1.如何将需求准确的转为软件的设计?
2.系统的架构与代码如何有效的体现我们的设计?
3.如何将领域逻辑与技术分离?
4.如何能够让团队人员的开发能够专注与业务,而不是技术本身?
5.如何交付高质量的软件,如何在出现问题时能够快速定位到代码?
6.如何快速响应需求的变更?
7.如何能够有一个框架或思想限定,让开发人员遵循一个约束,有节奏感的开发?

为了解决以上问题,软件行业提出了一个成熟的思想(或叫方法论):领域驱动设计(DDD)。通过DDD,我们能够很好的对需求应对到设计,能够让开发聚焦业务本身,能够让代码体现我们设计,能够让团队在一个框架内有节奏的开发。

有些开发人员或架构师也许了解过DDD,但总时认为很难落地,本系列文章就是通过一个大健康行业的直销系统实战案例,让大家了解如何能够基于DDD的思想和框架,开发一个业务系统。

本系列文章将达到以下几个目的:

1.熟悉DDD基本概念。
2.基于DDD基本概念构建一个轻量级的框架。
3.基于DDD设计与开发一个实际的直销系统的后端。
4.能够举一反三,开发其他的业务系统或产品。
5.本系列文章不涉及高性能、大并发的系统开发。

本系列文章需要你具备的技术基础:

1.熟悉C#。
2.熟悉http://Asp.nethttp://Asp.net Core。
3.熟悉EF或EF Core。
4.有一定的系统开发经验。

本系列文章大体的内容安排:

1.直销系统概述。
2.DDD基本概念、开发支持DDD基本概念的轻量级框架。
3.开发直销系统的产品上下文。
4.开发直销系统的经销商上下文。
5.开发直销系统的订单上下文。
6.简单了解对前端的其他支持、前端的开发。

直销系统需求(裁剪后,便于大家理解DDD应用即可)概述:

一.产品管理
1.产品上架,产品分为主产品与多个子产品规格。
2.子产品用于经销商购买。
3.子产品有价格与相应的PV(PV用于经销商购买产品后累加,用于核算经销商奖金)。

二.经销商发展
1.子经销商由上级经销商介绍并注册。
2.根据注册时的电子币确定子经销商的会员级别。
3.被注册的子经销商属于一个介绍层级。
4.一个经销商最多介绍两个子经销商。

三.产品下单
1.经销商通过电子币购买产品。
2.确定邮寄地址。
3.其他功能与普通电商类似。

搭建支持DDD的轻量级框架

要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。

一.什么是领域驱动设计(DDD)

首先要知道DDD是一种开发理念,核心是维护一个反应领域概念的模型(领域模型是软件最核心的部分,反应了软件的业务本质),然后通过大量模式来指导模型设计与开发。
DDD的一般过程是:首先通过软件需求规格说明书或原型生成一个领域模型(类、类的属性、类与类之间的关系);然后根据模式(应该如何分层?、领域逻辑写在哪?与持久化如何交互?如何协调多对象领域逻辑?如何实现逻辑与数据存储解耦等)指导来实现代码模型。

二.为什么使用DDD

DDD能应对复杂性与快速变化:

1.从技术维度实现分层:能够在每层关注自己的事情,比如领域层关注业务逻辑的事情,仓储关注持久化数据的事情,应用服务层关注用例的事情,接口层关注暴露给前端的事情。

2.业务维度:通过将大系统划分层多个上下文,可以让不同团队和不同人只关注当前上下文的开发。

3.时间维度:通过敏捷式迭代快速验证,快速修正。

三.DDD核心组件

1.界限上下文:

首先要将大系统划分层多个界限上下文,比如大健康行业直销系统可以划分为产品、经销商、订单等几个界限上下文,每个界限上下文有自己的领域逻辑、数据持久化、用例、接口等。每个界限上下文根据特点,具体实现方式又不同,比如有些界限上下文基本没有业务逻辑,就是增删改查,则可以使用CRUD最简单的模式;有些界限上线文有一定的业务逻辑,但对高并发、高性能没要求,则可以使用经典DDD模式;有些界限上下文有一定的业务逻辑,而且有高性能要求,则可以使CQRS模式。

2.实体:

有业务生命周期,采用业务标识符进行跟踪。比如一个订单就是实体,订单有生命周期的,而且有一个订单号唯一的标识它自己,如果两个订单所有属性值全部相同,但订单号不同,也是不同的实体。

3.值对象:

无业务生命周期,无业务标识符,通常用于模式实体。比如订单的收货地址、订单支付的金额等就是值对象。

4.服务:

无状态,有行为,通常就是一个用例来协调多个领域逻辑完成功能。

5.聚合:

通常将多个实体和值对象组合到一个聚合中来表达一个完整的概念,比如订单实体、订单明细实体、订单金额值对象就代表一个完整的订单概念,而且生命周期是相同的,并且需要统一持久化到数据库中。

6.聚合根:

将聚合中表达总概念的实体做成聚合根,比如订单实体就是聚合根,对聚合中所有实体的状态变更必须经过聚合根,因为聚合根协调了整个聚合的逻辑,保证一致性。当然其他实体可以被外部直接临时查询调用。

7.服务:

协调聚合之间的业务逻辑,并且完成用例。

8.仓储:

用于对聚合进行持久化,通常为每个聚合根配备一个仓储即可。仓储能够很好的解耦领域逻辑与数据库。

9.工厂

用于创建复杂的领域对象,能够将领域对象复杂的创建过程保护起来。

了解了DDD的好处与基本的核心组件后,我们先不急着进入支持DDD思想的轻量级框架开发,也不急于直销系统需求分析和具体代码实现,我们还少一块,那就是经典DDD的架构,只有了解了经典DDD的架构,你才能知道具体在哪层要实现哪些功能,编写哪些代码,具体在开发DDD的轻量级框架与具体模块代码实现时,才能做到有的放矢。

在这里需要说明的是,我们的大健康行业直销系统有一定的业务复杂性,没有高并发、高性能的需求,所以无论是经销商上下文、产品上下文还是订单上下文的具体实现,我们都将遵循经典DDD架构,而不是CRUD简单方式或CQRS DDD架构的方式。

传统三层架构以及问题:

clipboard.png

问题:
1.过分注重数据访问层,而不重视领域。
2.业务逻辑直接与数据访问层耦合,与领域为核心的DDD思想背道而驰。
3.没有一系列的模式与方法论指导这种分层架构的开发约束。

经典DDD架构:

clipboard.png

1.基础结构层:整个产品或系统的底层支撑

a.常用工具、支撑功能:这个.net core项目至少要实现以下的功能:Json配置文件的读取、WebApi返回给前端的基本格式对象的定义、Json序列化与反序列化、加密功能、依赖注入框架的二次封装等。

b.支持DDD框架:这个.net core 项目至少要实现以下的功能:聚合根接口定义、实体接口定义、值对象接口定义、仓储接口定义、仓储接口的EF Core顶层实现(工作单元模式)。

c.聚合根仓储实现:这个.net core项目严格来讲其实不属于基础结构层部分,只是由于习惯,把它放到基础结构层这个解决方案文件夹中。它其实是引用了领域层的领域对象,并且 从领域层对应的聚合根仓储接口中继承,然后实现领域对象持久化到数据库,这样,仓储实现是依赖衣领对象,领域对象与领域逻辑就不需要依赖仓储。领域模型才是系统真正的核心。

2.领域层:界限上下文的领域逻辑

a.首先要实现这个界限上下文的领域对象的POCO模型。

b.然后针对这个界限上下文的所有领域对象,建立每个领域对象自己的业务逻辑,注意的是,领域对象的业务逻辑最好不与仓储直接发生交互,就算领域逻辑要临时查询数据库也不要这样。

c.定义该界限上下文聚合根的仓储接口,这个接口代表的是聚合根与持久化打交道的基础约束,具体实现还是在基础结构层的聚合根仓储中实现,这样就实现了解耦。把聚合根仓储接口定义在领域层的意义是可以为领域层的调用方-应用服务层的用例提供对聚合持久化支持。

d.定义该界限上下文的EF Core上下文接口并实现,这样就通过映射关系,EF Core就可以处理领域对象与数据库表之间的映射了。

3.应用服务层:界限上下文的用例

a.某个上下文的应用服务层的某个用例,通过调用领域对象的领域逻辑,完成相关领域逻辑的实现。

b.领域逻辑完成后,应用服务层用例调用领域层的聚合根的仓储接口的方法,完成领域对象的预持久化。(应用服务通过基础结构层的依赖注入框架与Json配置文件找到聚合根仓储接口对应的实现)

c.应用服务层用例然后调用基础结构层的EF Core仓储接口的工作单元方式,完成真正的持久化。(应用服务通过基础接口层的依赖注入框架与Json配置文件找到顶层仓储接口对应的工作单元实现)

d.用例返回给接口层需要的前端所需的json对象格式。

4.接口层:非常薄的一层

a.只需要调用应用服务层用例

b.向前端返回所需的json对象格式

从上述架构特点可以看出,聚合根的仓储与领域逻辑完全解耦,是通过应用服务层的用例将他们协调起来完成功能。

我们讲了经典DDD架构对比传统三层架构的优势,以及经典DDD架构每一层的职责后,下面将介绍基础结构层中支持DDD的轻量级框架的主要代码。

这里需要说明的是,DDD轻量级框架能够体现DDD的思想即可,没必要做得很重,你也可以根据理解,自己实现支持DDD的框架。

1.实体、聚合根与值对象的顶层体现

实体顶层定义:
public interface IEntity

{
    string Code { get; set; }
    Guid Id { get; set; }
}

Id是一个未来存储到数据库表中的技术主键,Code是领域对象的唯一业务标识符。你也可以扩展这个接口,定义两个实体比较接口(未来实现就是比较两个实体如果Code一致,则代表两个实体相等)。

聚合根顶层定义:
public interface IAggregationRoot:IEntity

{

}

聚合根接口就是从实体接口继承,只是未来的用法可以在仓储中定义持久化时的领域对象必须从这个接口或继承了这个接口的抽象类继承下来的。

值对象顶层定义:

public interface IValueObject

{
    Guid Id { get; set; }
}

值对象接口只需要保留一个技术主键即可,它没有业务标识符。在数据库中,值对象可能作为单独表存储,也可以作为实体的一部分存储。你也可以扩展这个接口,定义两个值对象比较接口(未来实现就是比较两个值对象如果所有属性值一致,则代表两个值对象相等)。

工作单元顶层定义:
public interface IUnitOfWork

{
      void Commit();
}

工作单元接口就定义了一个提交方法,在具体实现时,其实就是对应的EF Core的整个聚合的事务提交方法。

仓储接口顶层定义:
public interface IRepository:IUnitOfWork,IDisposable

{
}

仓储接口从工作单元接口与资源释放接口继承,为未来的数据访问框架和可替换性提供顶层约束。

EF Core顶层仓储持久化实现:

public class EFCoreRepository : IRepository

{
     private readonly DbContext context;
     public EFCoreRepository(DbContext context)
    {
         this.context = context;
    }
     public void Commit()
    {
         try
        {
            context.SaveChanges();
        }
        catch(Exception error)
        {
             throw error;
        }
    }

    public void Dispose()
    {
        context.Dispose();
    }
}

从上述代码中可以看到,主要实现了仓储接口的Commit方法,其实就是使用了EF Core的DbContext数据访问上下文类的SaveChanges()事务提交方法,应用服务层的用例就可以获取到某个聚合根的当前状态,然后调用仓储接口的Commit方法,实现了整个聚合所有对象的一次性事务提交。

2.常用工具类的实现

我们还应该定义另一个项目,这个项目是整个系统都需要使用到的工具,其中至少应该包括http://Asp.net Core Json配置文件的读,Json序列化与反序列化,加密,依赖注入,返回给前端的对象格式定义等,这里先列出几个需要的,其他的在后面具体案例中在补充。

http://Asp.net Core Json配置文件读取:

Json配置文件会存储我们的一些配置信息,比如数据库连接字符串,微信AppId与AppSecure等,所以需要有功能支持Json配置文件的Key到Value的读取

public class AppSetting

{
      private static IConfigurationSection appsections = null;
      public static void SetAppSetting(IConfigurationSection section)
    {
        appsections = section;
    }
     public static string GetAppSetting(string key)
    {
         string str = "";
         if (appsections.GetSection(key) != null)
        {
            str = appsections.GetSection(key).Value;
        }
        return str;
    }
}

返回前端的对象格式定义:

我们的应用服务层将返回WebApi接口一定的数据格式,WebApi接口也会将这个数据返回给前端,前端拿到后就会做相应的处理。

public class ResultEntity<T>

{
    public bool IsSuccess { get; set; }
    public string Msg { get; set; }
    public T Data { get; set; }
    public int ErrorCode { get; set; }
    public int Count { get; set; }
}

public class BaseAppSrv

{
     protected ResultEntity<T> GetResultEntity<T>(T vobj,string msg="未成功获取到对象",int errorcode = 0)
    {
          var ueresult = new ResultEntity<T>();
          var issuccess = true;
          if(vobj is int && Convert.ToInt32(vobj) <= 0)
        {
               issuccess = false;
        }
         else if(vobj is bool && !Convert.ToBoolean(vobj))
        {
               issuccess = false;
        }
        else if(vobj is string && string.IsNullOrEmpty(Convert.ToString(vobj)))
        {
               issuccess = false;
        }
        if (!issuccess)
        {
              ueresult.Msg = msg;
              ueresult.ErrorCode = 200;
        }
        ueresult.IsSuccess = issuccess;
        ueresult.Data = vobj;
        return ueresult;
    }
}

未来所有的用例都将从BaseAppSrv继承,最终返回的格式都是ResultEntity<T>。

好了,基本的框架搭建好了,下一章就可以直接进入案例,看案例中如何通过DDD思想进行设计,并通过经典DDD架构与DDD轻量级框架进行实际业务系统的代码编写。

查看原文

赞 20 收藏 13 评论 2

流花飘原 赞了文章 · 2020-03-14

如何实现一款轻量级的可视化画布引擎

在很多定制化的可视化场景中,拖拽、缩放、全屏操作必不可少,尤其对于业务复杂的可视化需求,当画布内容足够多,可视区域显示不下时,就需要借助缩略图来一览全局。

本文将介绍一款轻量级的画布引擎ReScreen,ReScreen是集缩略图与画布操作为一体的轻量级绘图引擎,为React专门定制,统一封装了画布的操作和缩略图功能,支持对画布的全屏、复位、显示所有、重置、平移缩放等常见功能。

基于ReScreen,我们搭建了一个通用编排场景的demo。
image.png
可以在使用了之后,再阅读下文,体感会更强。

ReScreen 介绍

ReScreen具备如下的特性:

  • 支持缩放、拖拽等基本功能
  • 支持缩略图,提供缩略图的点击聚焦功能、缩略图的位置、大小、间距等样式
  • 支持缩略图的自定义传入,默认使用画布的缩小版
  • 支持是否启动鼠标滚动缩放
  • 支持缩放的范围设置
  • 支持锁定某一方向的拖拽
  • 支持控制按钮的自定义
  • 支持拖拽动画
  • 画布内容为SVG,或者原生DOM,目前暂未支持Canvas情况

ReScreen基于React与TypeScript开发,其属性参数为:

class Props {
  /** 画布内容的类型,默认为SVG */
  type?: 'SVG' | 'DOM' | 'CANVAS';
  /** 组件整体的尺寸,支持传入百分数 */
  height?: number | string;
  width?: number | string;
  /** 是否启动鼠标滚动缩放画布,默认为true */
  zoomEnabled?: boolean;
  /** 是否启动聚焦功能,0表示不启动,1表示单击触发,2表示双击触发 */
  focusEnabled?: number;
  /** 缩放范围 */
  minZoom?: number;
  maxZoom?: number; 
  /** 拖拽方向的锁定,默认为ALL */
  dragDirection?: 'ALL' | 'HOR' | 'VER';
  /** 是否需要缩略图,默认为true */
  needMinimap?: boolean;
  /** 支持自定义传入缩略图组件 */
  Minimap?: React.ReactElement<any>;
  /** 缩略图位置,默认为RT,右上角;-IN表示在画布的内部 */
  mapPosition?: 'RT' | 'RB' | 'LT' | 'LB' | 'RT-IN' | 'RB-IN' | 'LT-IN' | 'LB-IN';
  /** 缩略图和原图之间的大小,默认为20 */
  mapPadding?: number;
  /** 缩略图大小,默认为100px */
  mapWidth?: number;
  mapHeight?: number;
  /** 缩略图矩形的样式,svg语法 */
  mapRectStyle?: object;
  /** 按钮组件,如果不需要就不传 */
  Buttons?: React.ReactElement<any>;
  /** 由于画布元素的变化而引起的视图变化 */
  needRefresh?: boolean;
  /** 通知外层重置needRefresh为false */
  resetNeedRefresh?: () => void;
  /** 画布发生变化时的回调,对外暴露当前的缩放信息 */
  onScreenChange?: (transform: ZoomTransform) => void;
  /** 对外暴露画布操作函数 */
  getScreenHandler?: any;
}

实现原理详解

画布的缩放能力主要借助了d3-zoom,但整体上还是涉及一些数学计算。这里涉及包括画布上可以平移缩放操作,也包括在缩略图上进行反向的操作。缩略图采用的是DOM复制,每次画布操作发生变化时,就重新复制一份。当然也支持自定义缩略图组件。

基本原理

d3-zoom 里将问题进行了如下的抽象,假设当前画布缩放系数为 k,x 轴平移偏移量在当前缩放系数下为 x ,y 轴平移偏移量在当前缩放系数下为 y,(k,x,y)在点(P0,P1)处进行缩放,需要求得缩放后的(k',x',y'),在 SVG 中的表现就是

<svg width="w" height="h">
  <g transform="translate(x,y) scale(k)">
  </g>
</svg>
  • 通过鼠标滚动操作/放大按钮等等,求得缩放系数 k',在 d3.zoom 内置有缩放系数计算函数:

  • 将(P0,P1)还原到(k,x,y)=(1,0,0)时的坐标:

  • 由于(P0,P1)为缩放原点,它的偏移量是不变,由此求得 x' 与 y'

对于拖拽问题,k 值是不变的,拖拽仅改变 x 与 y 值,这块就比较简单了,通过鼠标拖拽的起点与重点,能得到相应变化量,将变化量给予 x 与 y 即可。

实现步骤

整体主要分成如下几个步骤:

1. 根据传入属性,计算(或者直接获取)画布可视窗口的大小screenWidth/screenHeight和缩略图的大小mapWidth/mapHeight

2. 计算画布内容全部映射到缩略图中所需要的变化值screenToMapTransform

3. 监听画布的zoom事件,用screenTransform记录画布当前的变换;

4. 监听缩略图可视矩形的zoom事件,用minimapTransform记录矩形当前的变换;

5. 画布的最终缩放效果transform = screenTransform * invert(minimapTransform),而缩略图可视矩形的变换为invert(transform)

具体涉及到的计算原理如下图:

image.png

image.png

image.png

image.png

这样就具备了最基本的缩略图缩放功能了。这里涉及到一个问题,当画布内容变化时,缩略图也需要适时地自适应,即重新获取一次screenToMapTransform

我们再简单介绍一下其他功能:

  • 复位:将screenTransformminimapTransform恢复到默认初始值,即设置为单位矩阵zoomIdentity
  • 重置:重置不仅具有复位的功能,还需要通知画布内部一切数据需要恢复到初始状态。
  • 显示全部:这个也比较简单,跟screenToMapTransform的过程类似。
  • 以画布中心缩放:假设P0为画布中心坐标,当前变换为transform,反求出在变换前P0的坐标P1;那么在新的transform1下,P1移到了P2,所以此时想要让画布的中心保持不变,就需要让画布平移P0 - P2的距离。如图所示:

image.png

总结

拖拽在可视化场景中是很常见的交互形式,想做好拖拽的交互其实不容易,社区里也有很多强大的可视化绘图引擎,如AntV的底层G引擎,提供了统一的渲染机制。

本文介绍的ReScreen是一种基于React的轻量级绘图引擎方案,目的是借助React生态的能力,更大程度地帮助用户实现业务需求,用户仅仅只关注业务的开发,降低了学习成本,达到开箱即用的效果。

ReScreen 是ReGraph体系中的重要一环,ReGraph 是结合基础操作层、渲染交互层、布局算法层三层结构,针对数据领域图表(以数、图为基础数据结构,带有数据业务属性与特征的可视化图表)提供的解决方案。基于ReGraph体系,我们已经支持了多个领域图表场景的开发,比如复杂的DAG场景、ERGraph、服务编排场景等。目前ReGraph正在逐渐完善与开放,如果您有兴趣,也欢迎提供宝贵的意见。

最后打一下广告:阿里巴巴数据技术及产品部-体验技术团队招大量高级前端开发工程师/前端技术专家,技术氛围好,大神多,妹纸也多。欢迎投递简历:perkin.pj@alibaba-inc.com

image.png

查看原文

赞 18 收藏 12 评论 4

认证与成就

  • 获得 3 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-30
个人主页被 387 人浏览