使用IoC来管理你的Vue应用

共 8572字,需浏览 18分钟

 ·

2020-10-26 18:14


本文来自于晓黑板前端技术的投稿

原文链接:https://juejin.im/post/6881883342623473677

伴随着现代应用功能越来越多,各个模块不可避免的相互依赖、引用,如果没有任何策略的堆代码,应用的维护会变成一种灾难。因此,有效的管理和解耦依赖变得很重要。本文从依赖注入的角度切入,尝试利用相关的理念来解决这个问题。

先看一个例子

假设我们有两个模块:一个实现 http 请求,另一个实现路由跳转。

// httpService.ts
export class HttpService {
    name = 'HttpService'
}
// routerService.ts
export class RouterService {
    name = 'RouterService'
}


现在有一个登录功能使用了上述两个模块:

// login.ts
export class Login {
    constructor() {
        this.httpService = new HttpService()
        this.routerService = new RouterService()
    }
}


在上面的代码中,为了实现登录功能,Login 类内部分别实例化了 HttpService和 RouterService。虽然上述代码可以正常工作,但是不是很灵活。假如修改HttpService 需要增加 token 信息:

// httpService.ts
export class HttpService {
    name = 'HttpService'
    constructor(token:string) {}
}


此时我们就需要编辑 Login 类,在 HttpService 实例化时增加 token 参数。假如我们想要给 RouterService 增加操作或者再次更新 HttpService,就不可避免每次都要重新编辑 Login 类。

为了解决现状,首先我们把依赖作为参数传递给模块:

// login.ts
export class Login {
    constructor(httpService: HttpService, routerService: RouterService) {
        this.httpService = httpService
        this.routerService = routerService
    }
}


这样就完成了 Login 和 HttpService、RouterService 的解耦,Login 不再亲自创建 httpService 和 routerService,而是使用他们。

然而这样还有新的问题:想象一下假如 HttpService 和 RouterService 在很多地方被调用,如果增加他们的依赖条件,我们就不得不改变所有调用他们的地方。

// routerService.ts
export class RouterService {
    name = 'RouterService'
    constructor(authService:AuthService) {}
}
// RouterService的依赖条件发生了改变,这时候就需要在下面的不同文件中修改,如果文件过多,这种方法就显得很不合适了。
// 并且我们发现出现了多个HttpService和RouterService实例。
// login.vue
const httpService = new HttpService(token)
const authService = new AuthService()
const routerService = new RouterService(authService) // 增加依赖条件
const login = new Login(httpService, routerService)
// list.vue
const httpService = new HttpService(token)
const authService = new AuthService()
const routerService = new RouterService(authService) // 增加依赖条件
const login = new Login(httpService, routerService)


因此,我们需要一个来帮助我们管理依赖的工具。
这就是依赖注入要解决的问题,先列一下我们要实现的目标:

  • 依赖的创建和查找交给第三方
  • 依赖本身的依赖可以自动被创建
  • 可以手动注册依赖
  • 依赖的类型不仅限于类,也可以是值,或者函数
  • 为了共享数据,保持依赖单例
  • 语法简洁,不需要写太多的代码

什么是IoC、DI

在实现我们的目标之前,我们需要先弄明白依赖注入涉及的相关概念。

控制反转

控制反转(Inversion of Control,缩写IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还要一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递(注入)给它。——维基百科

依赖注入

在软件工程中,依赖注入(Dependency Injection)的意思为,给予调用方它所需要的事物。“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入”。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让调用方直接获得依赖,这个是该设计的根本需求。——维基百科

控制反转概念中提到的调控系统就是我们要实现的目标中提到的第三方,即 IoC 容器。我们通过容器将A对象中用到的 B 对象在外部 new 出来并注入到A中,取代在 A 中显式的 new 一个 B 对象。从而达到设计的目的:解耦调用方和依赖,提高代码可读性以及代码重用性。

实现一个IoC容器

在了解完依赖注入相关的概念之后,我们来手动实现一个简单的 IoC 容器

1.定义容器接口

// interface.ts
export interface ContainerInterface {
  addProvider(token: Token,provider: any): void;
  getProvider(token: Token): T;
}

Container 类至少需要实现 addProvider() 和 getProvider() 两个方法。接着我们定义参数类型

2.定义 Token 和 Provider

// interface.ts
export interface Type extends Function {
  [INJECTED]?: Type<any>[] // 在3.3实现@Injectable()用到
  new (...args: any[]): T;
}
export type Token = string | Type


Token:DI令牌,它关联到一个依赖提供者,用来查找依赖。我们定义它的类型为联合类型,即可以是字符串也可以是函数类型。

Provider:一个提供者对象,定义了如何获取与 DI 令牌(token) 相关联的可注入依赖。这里我们不限制提供者的类型,可以是任意类型的值(any)

3.实现装饰器@Injectable()

3.1 装饰器

一个函数,用来修饰紧随其后的类或属性定义。装饰器(也叫注解)是 JavaScript 的一种语言特性,是一项位于 stage 2 的试验特性。

我们这里使用类装饰器 @Injectable() 来标记对象为可注入对象。

3.2 元数据反射

Reflect Metadata是ES7的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript在1.5+的版本结合refelct-metadata库已经支持,使用方法:

  • npm i refelct-metadata --save
  • tsconfig.json里配置emitDecoratorMetadata选项。


然后在项目中引入reflect-metadata后,就可以使用Reflect.getMetadata的API了。我们这里主要使用Reflect.getMetadata("design:paramtypes", target, key)方法来获取函数参数,记录依赖信息。

3.3 实现@Injectable()

了解了装饰器和元数据反射这两个前置条件的相关概念后,我们就可以来实现Injectable 函数了

// injectable.ts
import 'reflect-metadata'
import { Type } from './interface'
export const INJECTED = '__INJECTED_TYPES'
export function Injectable() {
  return function(target: any{
    // 记录前置依赖
    const outInjected = Reflect.getMetadata('design:paramtypes', target) as (Type<any> | undefined)[]
    const innerInjected = target[INJECTED]
    if(!innerInjected) {
      target[INJECTED] = outInjected
    } else {
      outInjected.forEach((argType, index) => {
        if(!innerInjected[index]) {
          target[INJECTED][index] = argType
        }
      })
    }
    return target
  }
}

4.实现Container

我们在前面定义好了ContainerInterface和两个关键的方法addProvider()getProvider(),下面我们就来分别实现

4.1 addProvider()

// container.ts
import { ContainerInterface, Token } from "./interface";
export class Container implements ContainerInterface {
  private _providers = new Map();
  
  addProvider(token: Token<any>, provider: any) {
    this._providers.set(token, provider);
  }
}

Container类具有一个私有变量_providers,类型为Map,保存所有的提供者。提供者类型为any,所以我们可以注册任意类型值的provider。addProvider()注册依赖。

4.2 getProvider()

getProvider(token: Token): T {
    if (this._providers.has(token)) {
      return this._providers.get(token);
    } else {
      if (isClassProvider(token)) {
        const instance = this.getInstanceFromClass(token as Type<any>);
        this.addProvider(token, instance);
        return instance;
      } else {
        throw new Error(`${token} is a normal string that cannot be instantiated`);
      }
    }
  }

通过getProvider()方法,传入token就可以拿到注册过的provider。这里我们增加了getInstanceFromClass方法,用来自动实例化class类型的依赖。

说明:在Angular DI的实现中,Provider为联合类型TypeProvider|ValueProvider|ClassProvider|ConstructorProvider| ExistingProvider|FactoryProvider|any[],考虑的场景比较全面,实现起来也很复杂。我们这里只是演示思路,所以只考虑class这一种类型,并且简化Provider的类型为any

4.3 getInstanceFromClass()

private getInstanceFromClass(provider: Type): T {
    const target = provider;
    if (target[INJECTED]) {
      const injects = target[INJECTED]!.map(childToken => this.getProvider(childToken));
      return new target(...injects);
    } else {
      if (target.length) {
        throw new Error(
          `Injection error.${target.name} has dependancy injection but,but no @Injectable() decorate it`
        );
      }
      return new target();
    }
  }

还记得我们在实现@Injectable()时通过元数据反射拿到的参数信息吗,当时被我们记录在了对象的[INJECTED]属性上面。target[INJECTED]类型为Type[],使用map()方法让数组中的每一个元素都调用getProvider()方法,递归获取所有的依赖,然后返回目标类的实例。

至此,一个简易版的 IoC 容器就制作完成了,让我们来测试一下是否可行吧。

// test.ts
import { Container } from './container'
import { Injectable } from "./injectable";
@Injectable()
class AuthService {
  name = 'authService'
}
@Injectable()
class RouterService {
  name = 'routerService'
  constructor(private authService: AuthService){}
}
const container = new Container()
const routerService = container.getProvider(RouterService)
console.log(routerService)


在浏览器中运行测试代码,可以在控制台中看到成功后的打印信息

在Vue中使用

在前文中我们实现了一个简易版的 IoC 容器,现在回到我们的主题“使用 IoC 来管理你的 Vue 应用”。

众所周知,.vue文件使用三段式代码分别实现templatescriptstyle,并基于component来拆分组合代码。但是当组件中js逻辑过多时,.vue文件就会变得比较臃肿,不利于维护。我相信很多人都碰到过这种情况,也都有各种的解决方案。

本文不讨论哪种方案更好,旨在通过依赖注入这个点来切入 Vue 项目,提供一种维护项目的思路。我们先来看一张图片


这张图片形象的说明了 IoC 容器扮演的角色,通过控制反转将业务模块中各个容易变化的部件抽象解耦,不同的模块去实现自己的定制需求,而通用代码不要重复开发。

这里我们使用插件来为 Vue 提供 IoC 容器的功能。

// plugin.ts
import { VueConstructor } from "vue";
import { Container } from "./index";
import { Type } from "./interface";
export default {
  install(Vue: VueConstructor, rootContainer: Container) {
    Vue.mixin({
      beforeCreate() {
        const { viewInject } = this.$options;
        if (viewInject) {
          const injects = viewInject;
          for (const name in injects) {
            this[name] = rootContainer.getProvider(injects[name] as Type<any>);
          }
        }
      }
    });
  }
};


我们将依赖的注入位置放在 Vue 实例的初始化选项里,类型定义为对象viewInject?: Object。之后在 rootContainer 里查找依赖,如果存在就返回,没有就自动实例化。

插件完成之后,我们就可以在 Vue 项目中使用 IoC 了。

首先在入口文件main.ts中安装插件

// main.ts
import Vue from 'vue'
import IocPlugin from './plugin'
import { Container  } from "./container"
Vue.use(IocPlugin, new Container())


然后我们创建两个文件list.vuelistService.ts

// list.vue
import ListService from './listService'
export default {
  name: 'list',
  viewInject: {
    listService: ListService
  },
  mounted() {
    console.log(this.listService.getList())
  }
}
// listService.ts
import { Injectable } from "./injectable";
@Injectable()
export default class ListService {
  getList(): number[] {
    const data = [1,2,3,4,5,6]
    return data;
  }
}
复制代码


在浏览器中运行上述代码,控制台会打印出来getList()的返回值

不局限于Vue

至此,我们已经实现了一个在 Vue 应用中使用 IoC 的 MVP 了。虽然还有很多功能没有实现,比如 provider scope,@Inject(),依赖的生命周期等,但这不妨碍我们理解 IoC 和 DI 的理念,解耦我们的项目。不局限于 Vue,你也可以在其他框架中使用 DI 。

参考资料:

  • Inversion of Control Containers and the Dependency Injection pattern
  • Dependency injection in JavaScript
  • Angular 文档




 


推荐阅读 
webpack 5 正式发布!
面试会遇到的手写 Pollyfill 都在这里了
一份9年工作经验大佬推荐的前端书单评测
React17新特性:启发式更新算法
React 状态管理库的battle (setState/useState vs Redux vs Mobx)

觉得本文对你有帮助?请分享给更多人

关注「前端之露」加星标,跟露姐学前端

商务合作请添加微信FE-ROAD-ON



别的小朋友都有人
点赞
了,我的呢?



浏览 51
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报