[关闭]
@my943813636 2018-11-14T02:55:30.000000Z 字数 17010 阅读 657

众智新农项目说明文档

名词介绍 功能介绍 项目结构 架构说明

目录

名词解释:

基础名词:

设备 -> 各种可操作的设备 如: 水泵 阀门 生长灯 卷帘 风机 等等

传感器 -> 各种环境值的采集器 如果: 土壤温度 土壤湿度 光照强度 空气温度 等等

阀门设备 -> 特别说明一下,属于设备的一种,接在水管上主要是用来控制浇水的,阀门设备不是你想开就可以打开的,在一些场景下 多个用户会公用同一个水泵(水源),如果想开启阀门需要等待别人浇水完毕(同时开始太多阀门水压会不够),公用设备(水泵)有空闲的时候才能开启阀门进行浇水,所以浇水的时候你可能还得排队等待别人用完了,才能开始打开阀门浇水。(开启阀门还得开始水泵才能真正的开始浇水,水泵的水压是有限的)

公共设备 -> 类似设备 目前就是指水泵,不过使用权被多个用户共享,涉及到操作权限的问题(多个用户对水泵进行操作的问题) 。 只有在管理员权限手动模式 才会允许手动进行控制开关,其他情况下是根据灌溉区域的状态进行自动管理公用设备开关的
公用传感器 -> 完全类似传感器 不过被多个用户共享,但是不会涉及到多用户共享的时候的冲突情况,因为它的状态只会随环境变化改变。

灌溉区域 -> 为了解决公用设备冲突的问题(eg: 一个用户想要关闭水泵,另一个用户想要浇水) 造成的冲突, 通过设置普通用户的权限为大棚拥有者,这样他们就无法操作公用设备(eg:水泵)了,通过服务器统一安排控制按秩序排队浇水。阀门抽象层代理,实现在共享同一个水泵的时候,等待其他用户(灌溉区域)浇水完成,排队轮到自己后才打开阀门和定时停止浇水的功能。

权限说明:

主要分为 管理员权限 大棚拥有者 2种。管理员权限下可以切换手动模式自动模式及操作公共的设备 (eg:水泵 )

手动/自动模式说明:

手动模式 : 直接操作设备 , 没啥好说的, 一切的控制都靠自己不可以定时关闭阀门 , 这个模式下如果还是管理员权限可以控制公共设备的开关

自动模式 : 阀门设备会被一个名为 灌溉区域的代理,阀门设备公用设备不能直接控制开启关闭了,从而保证公共设备的调度和实现多用户情况 , 按排队顺序(提交浇水时间)有秩序的进行排队浇水

功能简介

功能概述:

实现了基本的用户信息管理 , 设备/传感器状态显示 , 设备控制 , 一键启动/停止灌溉浇水 , 定时查询更新数据功能 , 简单的权限管理 , 手动自动模式切换功能 , 为用户提供基础的可视化移动端设备管理操作界面

功能模块

  • 用户
    • 登录
    • 注册
    • 忘记密码
    • 找回密码
    • 用户协议
  • 主页
    • 多系统切换
    • 模式切换 (需要管理员权限才可以看到)
    • 一键浇水入口 (需要在自动模式下才能看到)
    • 公用环境信息展示
    • 公用设备的管理 (管理员权限 手动模式 2个条件下才能进行操作)
    • 地块信息展示区
  • 地块管理
    • 环境信息展示
    • 灌溉区域管理 (自动模式下才可见)
      • 浇水时长选择框
    • 设备管理 (手动模式下会展示阀门)
  • 一键浇水 (自动模式下才可进入)
    • 时长选择
    • 灌溉区域展示
  • 设置
    • 修改密码入口
    • 退出登录
    • 刷新间隔设置

项目结构

./ 根目录

根目录中除了各个文件夹,主要是一些配置文件,这里提一下这个package.json

随着项目的进行,我们可能还会通过npm再给项目安装一些别的软件包,那如果有别的小伙伴在之后加进来,难道我们还要让他们一个个地安装吗?
当然不是,那样不仅麻烦,而且太容易出错了。
这便是package.json的作用了。

1. package.json

package.json是描述依赖包的json文件
当我们在开发时,想给项目添加一些必要的包,只需要执行 npm install --save ,就能将这个包名及对应的版本添加到 package.json中。

注:这里ionic在start一个项目的时候自动帮我们创建并更新了package.json

而之后的小伙伴,只需要在package.json所在的目录中,打开命令行执行 npm install 或者 cnpm install,就会自动解析package.json ,并将其中提到的依赖包都安装下来,项目就能正常跑起来了。

package-lock.json 是根据package.json 自动生成的详细的依赖的缓存 包括真实使用的版本号 仓库地址 顶级依赖所依赖的二级依赖 都会在这个文件中提现出来
eg:

  1. ...
  2. "@ionic/storage": {
  3. "version": "2.1.3",
  4. "resolved": "https://registry.npmjs.org/@ionic/storage/-/storage-2.1.3.tgz",
  5. "integrity": "sha512-/i3Vn2jNBqteAm5FuGCNei5oJlFQB2JYFkH3nR5f5i7X4kRz17XAsAKXVQjyR9wiye8HmxglIz05JsC92nYUjQ==",
  6. "requires": {
  7. "@types/localforage": "0.0.30",
  8. "localforage": "1.4.3",
  9. "localforage-cordovasqlitedriver": "1.5.0"
  10. }
  11. }
  12. ...

2.cordoava配置

config.xml是全局配置文件,在很多方面控制着cordova应用的行为。Config.xml是平台无关的,基于W3C网络应用标准的xml格式文件,扩展了cordova核心api、插件和特定平台的设置
更加高阶的使用请移步这里 : https://cordova.apache.org/docs/en/latest/config_ref/index.html

3.Android自动签名相关的

Build.json 和 myKey.jks 这个的生成和配置方式 在之前的教程有讲过 可以看[https://www.zybuluo.com/my943813636/note/1104836#2%E8%87%AA%E5%8A%A8%E7%AD%BE%E5%90%8D%E6%96%B9%E5%BC%8F]

4.其他的文件 剩余文件通过Ionic创建Cli支后就一直存在没有修改过 基本没有需要配置的情况

./src目录

在src目录的内部,便是原始的,未编译的代码,也是我们重点剖析的对象。
这就是Ionic应用程序的大部分工作。
当我们运行ionic serve,我们的代码里的src/会转换成浏览器能正确理解的JavaScript版本。

src/contract

目录存放的产量定义

src/modal

目录存放的是弹出窗口比page多了一个Controller

src/extend

目录存放的是工程的拓展内容

src/app

src目录中app目录一般存放的是项目的入口与切入点

src/assets

目录存放的是项目所需的各类资源 会一起打包进APK

src/pages

目录存放的是各个页面

src/component

目录存放的是可以复用的各种组件 , 会被多个页面应用,或者其他组件使用

src/directive

目录存放的是自定义的指令

src/pipes

目录存放的是自定义的管道 主要是用于将复杂的原始数据翻译成易于用户理解的文本或其他数据给用户

src/transition

目录存放的是自定义的页面的转场动画

src/theme

目录则是样式调整

src/index.html

  1. <ion-app></ion-app> //附近(上下)有两个较为重要的脚本:
  2. <script src="cordova.js"></script>
  3. <script src="build/main.js"></script>

src/index.html是应用程序的主要入口点,其目的是设置脚本,CSS和引导,或开始运行我们的应用程序。
我们不会花太多时间在这个文件中。
不过也有值得注意的地方:
cordova.js 将在本地开发期间404,因为它在Cordova的构建过程中被注入到您的项目中。
build/main.js 是一个包含Ionic,Angular和您的应用程序JavaScript的连接文件。

src/setvice-worker.js

service worker是一段脚本,与web worker一样,也是在后台运行。作为一个独立的线程,运行环境与普通脚本不同,所以不能直接参与web交互行为。native app可以做到离线使用、消息推送、后台自动更新,service worker的出现是正是为了使得web app也可以具有类似的能力。独立于页面的一个后台线程不可以直接控制页面Dom元素,但可以在独立线程 进行复杂数据的计算和进行 消息推送等功能 目前使用不需要太多关注,还用不到该功能
了解更多

架构说明

一)懒加载

Ionic3默认采用懒加载机制 , 所以本项目使用的也是懒加载 理解懒加载的使用 和 Angular 的惰性模块 特性模块 对接下来对本工程的整体了解十分重要,什么是懒加载呢?当我们第一次进入应用,会加载app.module,如果没有采用懒加载,那么app.module里的所有东西都会被加载,很显然这是不太合适的,因为里面的组件和服务我们在第一次进入应用时并没有全部使用,可能只使用了很少的一部分,那么对于比较大型的应用来说,由于页面和其他的服务,指令,管道比较多,就会造成比较用户体验不好的影响。
这个时候我们需要把应用分为一个个module,各自引入自己用到的东西,不要管别的模块。那么第一次进入应用,就只加载很少的app.module和root page对应的module,当我们进入其他页面的时候,才会去加载相应的module。这样的好处是项目代码结构清晰,易于维护,易于开发,而且第一次加载速度比较快。
当我们使用Ionic3的CLI来新建页面,默认的会是懒加载的结构,甚至不止页面,所有的component,service,directive都默认的是懒加载。
创建的component 一般是咱这个样子的 src/component/farmlands-preview 包含4个文件
html模板文件
ts代码文件
scss样式表
还有一个以.module.ts结尾的 模块描述文件

Ionic3采用IonicPage()和IonicPageModule引入了一个新的概念——page,我们通过CLI新建的Page可以通过很简单的方式来跳转:

  1. this.navCtrl.push('FramlandPage');

不需要像Ionic2那样引入组件,只需要使用引号,里面写上页面名字,就可以实现跳转,
这是因为IonicPageModule将这个页面Module声明过了

@IonicPage() 注解是用于设置加载的优先级 src/pages 目录下的都应该带上这个注解 官方解释如下

If preloading is turned on, it will load the modules based on the value of priority. The following
values are possible for priority: "high", "low", and "off". When there is no priority, it
will be set to "low".
All deep links with their priority set to "high" will be loaded first. Upon completion of loading the
"high" priority modules, all deep links with a priority of "low" (or no priority) will be loaded. If
the priority is set to "off" the link will not be preloaded. Setting the priority is as simple as
passing it to the @IonicPage decorator:

如果打开预加载,它将根据优先级值加载模块。下列值可能: "high", "low", and "off". 。当没有设置优先权时,它将被设置为“低”。所有优先级设置为“高”的深度链接将首先加载。加载完成后“高”优先级模块,所有优先级为“低”(或无优先级)的深度链路将被加载。如果优先级设置为“关闭” 他将不会预加载

示例

  1. @IonicPage({
  2. name: 'my-page',
  3. priority: 'high'
  4. })

更多关于懒加载的内容请参考 :
1. https://angular.cn/guide/lazy-loading-ngmodules
2. https://angular.cn/guide/sharing-ngmodules
3. https://angular.cn/guide/ngmodule-api
4. 1. https://segmentfault.com/a/1190000011733022
5. http://www.cnblogs.com/gavin-cn/p/6943584.html

二) API请求生命周期绑定

在用RxJs的时候当被观察者(Observable)和观察者(Observer)产生订阅关系后没有及时释放这种subscription就很容易产生内存泄漏,一个典型的场景就是使用RxJs发起网络请求,此时应用程序被杀掉,这种订阅关系就没有得到及时释放。当然这种情况在onDestroy中手动进行判断也行。如果是这种场景,发起的网络请求还没成功返回,此时应用进入后台,这时候就算请求成功返回也不应该更新UI,这种情况在使用RxJava的情况下怎么处理?
仿照Java的Rxlifecycler 库 实现了一个简单的生命周期绑定功能 , 在页面退出的时候自动结束未完成订阅的请求 , 防止造成内存泄漏 或者 异常回调访问的视图已经被销毁而引起应用崩溃

src\extend\lifecycle\component-lifecycle-owner.ts

通过与页面的生命周期时间回调绑定 保存和转发页面的生命周期状态给后面的LifecyclerOperarte 使用。 所有想要拥有绑定生命周期的组件或者页面都需要继承与本组件。

  1. export class ComponentLifecycleOwner implements ILifecycleOwner, ILifecycleEvent, AfterViewInit, OnDestroy {
  2. lifecycleOwner: BehaviorSubject<LifecycleEnum> = new BehaviorSubject(LifecycleEnum.OnInit)
  3. lastEvent: LifecycleEnum = LifecycleEnum.OnInit
  4. viewController: ViewController
  5. constructor(protected injector: Injector) {
  6. this.viewController = injector.get(ViewController)
  7. }
  8. //自定义回调封装
  9. onCreate() {
  10. }
  11. onEnter() {
  12. }
  13. onLeave() {
  14. }
  15. onDestroy() {
  16. }
  17. //初始化入口
  18. ngAfterViewInit(): void {
  19. this.lastEvent = LifecycleEnum.OnInit
  20. this.onCreate()
  21. this.subscribeLifeEvent()
  22. }
  23. ngOnDestroy(): void {
  24. this.lastEvent = LifecycleEnum.OnDestroy
  25. this.onDestroy()
  26. this.lifecycleOwner.next(LifecycleEnum.OnDestroy)
  27. }
  28. //注册页面生命周期观察者
  29. private subscribeLifeEvent() {
  30. let instance = this.viewController.instance as PageLifecycleOwner
  31. //实时转发
  32. instance.lifecycleOwner.subscribe(
  33. (v) => {
  34. switch (v) {
  35. case LifecycleEnum.onEnter:
  36. this.lastEvent = LifecycleEnum.onEnter
  37. this.onEnter()
  38. this.lifecycleOwner.next(LifecycleEnum.onEnter)
  39. break
  40. case LifecycleEnum.onLeave:
  41. this.lastEvent = LifecycleEnum.onLeave
  42. this.onLeave()
  43. this.lifecycleOwner.next(LifecycleEnum.onLeave)
  44. break
  45. }
  46. }
  47. )
  48. }
  49. }

在其内部维护了一个 BehaviorSubject 对象。Subject 对象相当于一个管道,一边可以接收数据,从另外一边发射出去。而 BehaviorSubject 继承自 Subject,并且还多了一项特性,在新的观察者订阅时,会首先发射一个最近发射过的元素。 参考 http://reactivex.io/RxJava/javadoc/rx/subjects/BehaviorSubject.html
其次,在 组件 到达各个生命周期的时候,lifecycleOwner 会接收到对应的生命周期事件。

src\extend\lifecycle\lifecycle-operate.ts

实现给Rxjs 添加一个扩展函数 以免破坏链式调用的结构

  1. declare module 'rxjs/Observable' {
  2. interface Observable<T> {
  3. bindLifecycle: typeof bindLifecycle
  4. }
  5. }
  6. Observable.prototype.bindLifecycle = bindLifecycle
  7. export function bindLifecycle<T>(this: Observable<T>, lifecycleOwner: ILifecycleOwner, until: LifecycleEnum = LifecycleEnum.OnDestroy): Observable<T> {
  8. return this.lift(new LifeSourceOperator(lifecycleOwner, until))
  9. }
  10. //生命周期绑定转发
  11. class LifeSourceOperator<T> implements Operator<T, T> {
  12. constructor(public lifecycleOwner: ILifecycleOwner, public until: LifecycleEnum) {
  13. }
  14. call(subscriber: Subscriber<T>, source: Observable<T>): TeardownLogic {
  15. source.subscribe(new LifeSourceSubscriber(subscriber, this.lifecycleOwner, this.until))
  16. }
  17. }
  18. //生命周期绑定实现
  19. class LifeSourceSubscriber<T> extends Subscriber<T> {
  20. data: T = undefined
  21. private version = 0
  22. private pendingVersion = 0
  23. private isActive = false
  24. private lifeSubscription: Subscription
  25. constructor(subscriber: Subscriber<T>, public owner: ILifecycleOwner, public until: LifecycleEnum) {
  26. super(subscriber)
  27. this.subscribeLifecycle()
  28. }
  29. next(value?: T): void {
  30. this.data = value
  31. this.pendingVersion++
  32. this.considerNotify()
  33. }
  34. protected _error(err: any): void {
  35. super._error(err)
  36. this.lifeSubscription.unsubscribe()
  37. this.lifeSubscription = undefined
  38. this.isActive = false
  39. this.destination = undefined
  40. this.owner = undefined
  41. this.until = undefined
  42. this.data = undefined
  43. }
  44. protected _complete(): void {
  45. super._complete()
  46. this.lifeSubscription.unsubscribe()
  47. this.lifeSubscription = undefined
  48. this.isActive = false
  49. this.destination = undefined
  50. this.owner = undefined
  51. this.until = undefined
  52. this.data = undefined
  53. }
  54. private subscribeLifecycle() {
  55. this.lifeSubscription = this.owner.lifecycleOwner.subscribe(this.onStateChange.bind(this))
  56. }
  57. private onStateChange(status: LifecycleEnum) {
  58. if (status >= this.until) {
  59. this._complete()
  60. return
  61. }
  62. this.isActive = status <= LifecycleEnum.onEnter
  63. this.considerNotify()
  64. }
  65. private considerNotify() {
  66. if (!this.isActive || this.pendingVersion <= this.version) return
  67. this.version = this.pendingVersion
  68. this._next(this.data)
  69. }
  70. }

这里是整个代码中最核心、最巧妙的部分 首先第二个参数是 lifecycleOwner,也就是 LifecycleComponent 里的那个 BehaviorSubject 对象,这里作为 Observable 来使用。它会不断发射 LifeCyclerEvent,也就是 组件里的生命周期事件。

先介绍下里面用到的几个操作符:
- share:简单来说就是让 Observable 支持多订阅。
- skip:跳过 Observable 发射的前N项数据。
- takeUntil:当第二个 Observable 发射了一项数据或者终止时,丢弃原始 Observable 发射的任何数据。
- first:传入一个 Func1 给 first, 发射出能够使得这个 Func1 为 true 的元素。

当两个数据源的数据相等时,即 component 到达需要被销毁的生命周期时,返回 true,发射出一个元素。此时请求就会被丢弃,即终止掉这个Observable

通过以上代码就完成了Observable 自动绑定到Component的生命周期,并且自动结束

用法

  1. @Component({
  2. selector: 'device-display',
  3. templateUrl: 'device-display.html',
  4. host: {
  5. '[class.invalid]': 'deviceModel?.deviceVo?.state == -1'
  6. }
  7. })
  8. export class TestComponent extends LifecycleComponent { //1
  9. ...
  10. doRequest(){
  11. this.testProvider.testRequest()
  12. .bindLifecycle(this) //2
  13. .subscribe(
  14. ...
  15. )
  16. }
  1. 首先让组件继承LifecycleComponent 实现生命周期事件的转发
  2. 让这个Observable 绑定到这个生命周期

如此就实现了将http请求与页面的生命周期关联 从而避免内存泄漏或控制以及被销毁的视图从而引发的崩溃

三) 通用API请求处理

用途

这个部分主要是用于
1. 将服务器的返回原生数据解析成实体类带实际的类型 配合 json2typescript 三方库实现
2. 将成功的请求与失败的请求分开 通过rxjs 的 next 和 error 在不同的地方进行回调 , 减轻接口调用时判断失败情况的负担
3. 将登录成功 和 退出登录 token失效 做成全局事件 触发 页面跳转/处理登录状态改变的特殊逻辑
4. 添加通用的请求头(代码实现了该功能,但是实际上用Cookie的方式用不到 后面改成Token授权的时候就能用到了)
5. 获取服务器的响应头 目前是Date 用于计算服务器时间

2. 源码解析

1. src/app/app.module.ts

  1. @NgModule({
  2. ...
  3. imports: [
  4. ...
  5. HttpClientModule //1
  6. ...
  7. ],
  8. ...
  9. providers: [
  10. {provide: HTTP_INTERCEPTORS, useClass: BaseInterceptor, multi: true} //2
  11. ]
  12. ...
  13. })

在程序的主模块导入相关的依赖 HttpClientModule
1 -- 为全局 提供HttpClient服务
2 -- 提供一个请求拦截器 拦截器的代码在下面有讲到 , 值得注意的地方是 multi 要设置为true 因为这个拦截器的Token是相同的 但是提供者有多个,这样在HttpClient内部获取拦截器的时候 使用一个Token值 就会获取一个拦截器列表 , 在这里除了我们定义的一个拦截器外
还有一个出站(第一个) 和 一个入站(最后一个) 的拦截器在HttpClientModule 里面定义了 , 我们也可以定义多个拦截器 , 他们是链式调用的 请求服务器 的顺序 会按照你定义的拦截器 从上到下 逐个 调用 返回的时候 按相反的顺序

关于 Multi Provider 的详细信息可以看这个 : https://zhuanlan.zhihu.com/p/25658947
关于 HttpClient 更多的使用方式可以看这个 : https://segmentfault.com/a/1190000010116848

2. src/providers/base-interceptor.ts

  1. //服务器时间差
  2. let diffTime = 0
  3. export function getServerTime() {
  4. return Date.now() - diffTime
  5. }
  6. export class BaseInterceptor implements HttpInterceptor {
  7. intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  8. let headers = req.headers
  9. if (headers.get('NoAppToken')) headers = headers.delete('NoAppToken')
  10. else if (TokenManageProvider.ev.value) headers = headers.set('AppToken', TokenManageProvider.ev.value.token)
  11. return next.handle(req.clone({headers}))
  12. .map((value) => {
  13. if (value.type === HttpEventType.ResponseHeader) {
  14. diffTime = Date.now() - Date.parse(value.headers.get('Date'))
  15. if (Number.isNaN(diffTime)) diffTime = 0
  16. }
  17. return value
  18. })
  19. }
  20. }
  21. export const HttpInterceptorProviders = [
  22. {provide: HTTP_INTERCEPTORS, useClass: BaseInterceptor, multi: true},
  23. ]

这里就是拦截器的代码了 在这里主要实现了 2个东西 一个是添加一个 withCredentials 请求头, 允许不同源间的cookie传递(这个是目前的登录授权方式) , 后面改成 TOKEN 的方式就是自己添加Token的请求头即可 , 一个是从服务器的响应头中获取 Date 从而获取服务器的当前时间。值得注意的地方是 -- 3 这里给req 添加 withCredentials 的使用用了Clone 而不是直接改 Header 这个是因为 Req里面的字段全是 ReadOnly 注解的 没法修改 , 所以 如果需要修改 Req 需要通过Clone 然后 重写 里面的内容

1 -- 继承拦截器的接口
2 -- 实现拦截器的方法 , 这个地方有2个参数 第一个是 req 当前的 出站 请求头 , 和 next 下一个请求拦截器
3 -- 修改原始请求 , 请求对象和响应对象必须是不可修改的 (immutable)。因此,我们在修改请求对象前,我们需要克隆原始的请求对象。
4 -- 处理事件的拦截
5 -- 如果事件的类型 等于 响应体 就获取Date头 转化为服务器的时间

3. src\extend\api-operate.ts

  1. function apiOperate<T>(this: Observable<any>, type: {new(): any}, baseLike: boolean = true): Observable<T> {
  2. return this.pipe(
  3. catchError((v) => {
  4. e(v)
  5. return ErrorObservable.create(RESULT_ERR)
  6. }),
  7. timeoutWith(20000, ErrorObservable.create(TIMEOUT_ERR)),
  8. switchMap((v) => handleResponse(v, type, baseLike)),
  9. )
  10. }
  11. function handleResponse<T>(data: any, type: {new(): T}, baseLike: boolean): Observable<T> {
  12. return baseLike ? handleBaseLikeResponse(data, type) : handleOtherResponse(data, type)
  13. }
  14. //忽略错误码只解析数据
  15. function handleOtherResponse<T>(data: any, type: {new(): T}): Observable<T> {
  16. return parseJson(data, type)
  17. }
  18. //带错误码的服务器
  19. function handleBaseLikeResponse<T>(data: any, type: {new(): T}): Observable<T> {
  20. if (data.code !== ErrorCode.OK) {
  21. if (data.code === ErrorCode.OAUTH_ERR) TokenManageProvider.cleanUserOauth()
  22. return _throw(data)
  23. }
  24. return parseJson(data.data, type)
  25. }
  26. function parseJson<T>(data: any, type: {new(): T}): Observable<T> {
  27. if (type && type.prototype['__jsonconvert__mapping__']) {
  28. try {
  29. if (typeof data === 'string') data = JSON.parse(data)
  30. let deserialize = new JsonConvert().deserialize(data, type) as T
  31. return Observable.create((v) => v.next(deserialize))
  32. }
  33. catch (v) {
  34. e(v)
  35. return _throw(JSON_ERR)
  36. }
  37. }
  38. else if (type && (data === undefined || data === null)) return _throw(JSON_ERR)
  39. else return Observable.create((v) => v.next(data as T))
  40. }
  41. declare module 'rxjs/Observable' {
  42. interface Observable<T> {
  43. apiOperate: typeof apiOperate
  44. }
  45. }
  46. Observable.prototype.apiOperate = apiOperate

原本想把错误处理 服务器数据转化为实体类(apiOperate.ts) 通过 拦截器去做 但因为 把原始类型转换成实体类 需要一个Type(数据对应的实体类) 在拦截器里面无法获取这个 Type 所以做成了Rxjs对象的拓展函数 apiOperate 进行处理。 这段代码设置了 请求无法发出 请求超时时间设置 请求成功的数据转换为实体类 和 服务器返回的错误码 这4个主要功能。

1 -- 为Rxjs的Observable对象添加一个拓展的成员函数 apiOperate 方便链式调用
2 -- 如果请求没法发出去 一般是网络中断了 就返回一个内部的错误码 "无法连接服务器"
3 -- 设置请求的超时时间 60s
4 -- 能进入这说明 服务器响应没有问题 请求没超时 , 将原始的数据流经过转换函数 转换为最终的结果 下面就开始进入 数据真正的处理流程
5 -- 错误处理函数 data 服务器返回的原始数据类型 , type 是数据对应的本地实体类 对象
6 -- 如果服务器的响应code 不是OK 那就进入错误的转换流程 , 第33行 如果 是登录错误 那就通过TokenManageProvider 转发这个错误 让全局订阅了登录改变时间的观察者都能收到这个错误 让用户去下载最新版本的APP , 45行 把错误通过 Observerable 的 error 发射出去 , 方便订阅者把出错的请求和成功的请求分开处理。
7 -- 如果服务器返回的Code 是 ok 那就把 响应的内容解析成实体类 通过 json2typescript 这个库来实现这种能力 吧data 和 type 穿进去 出来就是 带有类型的和数据 type 实体类 , 然后 进入 44行把内容 发射出去。 如果type的类型为 undefined 那就不需要进行格式转化 , 直接将内容传递出去即可,因为 json2typescript 不能处理 undefined 的 date , 所以在这个 对 undefined 进行特殊处理 。
8 -- 因为 47 行的格式映射可能会出现异常 所以在这个地方捕获一下异常 然后 把这次响应 当成 错误 发射出去 , 错误码为内部的Json_err 数据处理错误 同时在控制台中打印错误的原因

使用

使用过程很简单 只需要2部

1. 注入httpClient 服务

  1. constructor(public http: HttpClient) {
  2. }

2. 为请求添加apiOperate

  1. @JsonObject
  2. export class DeviceVoREntity {
  3. @JsonProperty("id") id: number = 0;
  4. @JsonProperty("type") type: number = 0;
  5. @JsonProperty("name") name: string = "";
  6. @JsonProperty("state") state: number = -1;
  7. }
  1. this.http.post(URL)
  2. .apiOperate<DeviceVoREntity>(DeviceVoREntity);

通过 apiOperate 就可以吧原始的服务器类型转换为DeviceVoREntity 实体类

3. 订阅请求结果

  1. this.deviceProvider.operateDevice(this.deviceModel.deviceVo.id, S2C_CMD_START_DEV, undefined, runTime)
  2. .bindLifecycle(this) //生命周期的绑定 在上一节有讲到
  3. .subscribe(
  4. (v) => {
  5. //成功的处理
  6. },
  7. (e) => {
  8. //出现错误的处理
  9. }
  10. );

json2typescript DEMO

json2typescript 的功能十分的强大很好用,可以在typescript 这种半强类型的语言中 体验到强类型语言 带来的IDE静态类型检测带来的好处 提高开发效率! 还有两个特别好用的是 如果字段服务器没有给出 可以给这个字段附一个默认值 和 把原始数据通过TypeConvent 转换为更加适合使用的实体类 。 具体使用的方式可以看看官网中的介绍

请先了解用法再看下面给出的例子
https://github.com/dhlab-basel/json2typescript

下面对一个具体的使用给出一个demo

服务器给的原始数据
一个DcPoints列表 下有多个DeciceVo 里面又 对应了 一个 SensorValues 列表 下有多个 传感器数据 type value (多值传感器 会对应多组传感器数据)

转换后的数据
dcPoint 下面直接对应 传感器值 而且按传感器的类型进行排序 这样对于在视图上进行数据渲染 更加方便 不需要在 使用的时候再进行转换了

转换器类

  1. @JsonConverter
  2. export class DcPointToSensorValue implements JsonCustomConvert<SensorValueCEntity[]> {
  3. serialize(data: SensorValueCEntity[]): any {
  4. return undefined;
  5. }
  6. deserialize(data: DcPointsREntity[]): SensorValueCEntity[] {
  7. let jsonConvert = new JsonConvert(undefined, undefined, true);
  8. let dcPoints: DcPointsREntity[] = jsonConvert.deserialize(data, DcPointsREntity);
  9. let sensorValues: SensorValueCEntity[] = [];
  10. for (let dcPoint of dcPoints) {
  11. for (let sensor of dcPoint.deviceVo.sensorValues) {
  12. if (dcPoint.deviceVo.state != DEV_STATE_OK) {
  13. sensor.value = VALUE_INVALID
  14. }
  15. sensor.tip = dcPoint.name;
  16. sensor.value = sensor.value == VALUE_INVALID ? "--" : (Number(sensor.value) / (sensor.type == CO2_CONC || sensor.type == ILLU_N ? 1 : 10)).toFixed(1);
  17. if (sensor.value.endsWith(".0")) sensor.value = sensor.value.slice(0, sensor.value.length - 2);
  18. sensorValues.push(sensor)
  19. }
  20. }
  21. return sensorValues;
  22. }
  23. }

服务器给出的传感器值类型

  1. @JsonObject
  2. export class SensorDeviceVoREntity extends DeviceVoREntity {
  3. @JsonProperty("sensorValues", [SensorValueCEntity]) sensorValues: SensorValueCEntity[] = Array<SensorValueCEntity>();
  4. }
  1. @JsonObject
  2. export class DcPointsREntity {
  3. @JsonProperty("id") id: number = 0;
  4. @JsonProperty("name") name: string = "";
  5. @JsonProperty("deviceVo", SensorDeviceVoREntity, true) deviceVo: SensorDeviceVoREntity = new SensorDeviceVoREntity(); //1
  6. }

1 -- 第三个参数表示 这个的值可以为空 , 如果空就使用默认的 SensorDeviceVoREntity

  1. @JsonObject
  2. export class SensorValueCEntity {
  3. @JsonProperty("type") type: number = 0;
  4. @JsonProperty("value") value: number | string = VALUE_INVALID;
  5. tip: string;
  6. }

实际想要的传感器类型 只需要一个传感器的类型 , 一个 传感器值 , 一个备注


用法

  1. @JsonObject
  2. export class FarmlandsCEntity {
  3. ...
  4. @JsonProperty("dcPoints", DcPointToSensorValue, true) dcPoints: SensorValueCEntity[] = Array<SensorValueCEntity>();
  5. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注