如何使用依赖注入提升前端代码质量

3,073 阅读4分钟

依赖倒置、控制反转,依赖注入傻傻分不清

依赖倒置、控制反转和依赖注入都是面向对象中常见的概念,它们有一定的联系和区别,但是大家容易混淆这三个名词,搞错概念,接下来给大家讲讲他们的区别

依赖倒置

依赖倒置(Dependence Inversion Principle,DIP)一般是指SOLID原则中的D原则,是指高层模块不应该依赖底层模块,两者都应该依赖其抽象。 同时,抽象不应该依赖细节,细节应该依赖抽象。简单来说,就是依赖抽象,而不是依赖具体实现。比如Class A 依赖 Class B,这是直接依赖,如果Class A依赖 interface B,而Class B实现了interface B,这就是依赖倒置了

控制反转

控制反转(Inversion of Control,IoC)也是一种面向对象编程的设计原则,它通过反转调用方和被调用方之间的关系,实现解耦合。 比如比如Class A 依赖 Class B,在A内部实例化了B。这样两个类就耦合在一起,如果是通过外部实例化B,然后在注入到A中,这样A和B就解除了耦合

依赖注入

依赖注入(Dependency Injection,DI)则是一种实现控制反转的方式,通过将被依赖对象的创建和管理由依赖方转移到外部容器(或者框架),然后将依赖的对象注入到需要使用的地方,实现解耦合。 在依赖注入中,组件不会自己创建或管理它所依赖的对象,而是由外部的组件将依赖的对象注入到它的构造函数、属性或方法中。

例子

下面的例子使用自己实现的DI库@lujs/di,源码非常简单,只有几百行,很适合上手学习,觉得有帮助的同学记得给个star

例子1

假设我们有一个用户登录界面例子来自从零开始构建用户模块:前端开发实践,在登录界面需要这些页面状态和方法。

View State:

  • loading: boolean; 页面loading
  • mobile: string; 输入手机号
  • code: string; 输入验证码

methods:

  • showLoading
  • hideLoading
  • login

我们为此创建了一个PagePresenter类,而这个类依赖于UserService这个服务类,UserService是提供一个loginWithMobile,用于实现发起http请求,登录校验等功能的服务类. 代码如下:

class PagePresenter extends Presenter<IViewState> {
  private userService: UserService = new UserService()
  constructor() {
    super();
    this.state = {
      loading: false,
      mobile: '',
      code: '',
    };
  }

  _loadingCount = 0;

  showLoading() {
    if (this._loadingCount === 0) {
      this.setState((s) => {
        s.loading = true;
      });
    }
    this._loadingCount += 1;
  }

  hideLoading() {
    this._loadingCount -= 1;
    if (this._loadingCount === 0) {
      this.setState((s) => {
        s.loading = false;
      });
    }
  }

  login = () => {
    const { mobile, code } = this.state;
    this.showLoading();
    return this.userService
      .loginWithMobile(mobile, code)
      .then((res) => {
        if (res) {
          message.success('登录成功');
        }
      })
      .finally(() => {
        this.hideLoading();
      });
  };
}
export class UserService {
  /**
   * 手机号验证码登录
   */
  loginWithMobile(mobile: string, code: string) {
    // mock 请求接口登录
    ...
  }
  
}

在上面的PagePresenter,我们new了一个UserService作为PagePresenter的属性。这样两个类就紧紧的耦合在一起,阻碍了以后的维护和扩展。

这时候就需要引入依赖注入技术了。我们把代码改写成下面这,不在PagePresenter中初始化UserService,而是把它当做一个参数注入进去

class PagePresenter extends Presenter<IViewState> {
  constructor(private userService: UserService) {
...

使用的时候就再实例化参数,这样两个类就解耦了

const u = new UserService()
const p = new PagePresenter(u)

不过这样使用起来还是不方便,要是框架可以自动帮我初始化UserService并注入就更好了,比如像下面这样

const p = container.resolve(PagePresenter)

于是就有了一大批依赖注入框架,本人也实现了一个@lujs/di,API是参考TSyringe实现的。
使用方法如下,需要在类前面加一个装饰器injectable,表明当前类需要使用依赖注入功能

@injectable()
class PagePresenter extends Presenter<IViewState> {
  constructor(private userService: UserService) {

injectable会在运行阶段找到PagePresenter所依赖的类并标记起来。 在需要初始化的时候,帮你初始化依赖的类,并自动注入到目标中

在前端中的应用

为了在前端中使用这些设计原则,我顺手实现了一个包含状态库、IOC容器的辅助库「clean-js」,可以用在react和vue中

一个简单的例子

// 具体的业务类
export  class NameService {
  getName() {
    return Promise.resolve('name')
  }
}
import { injectable, Presenter } from "@clean-js/presenter";
import { NameService } from './name.service.ts'

interface IViewState {
  loading: boolean;
}

const defaultState: () => IViewState = () => {
  return {
    loading: false,
  };
};

@injectable()
export class IndexPresenter extends Presenter<IViewState> {
  constructor(public nameS: NameService) {
    super();
    this.state = defaultState();
  }

  showLoading() {
    this.setState((s) => {
      s.loading = true;
    });
  }
  
  hideLoading() {
    this.setState((s) => {
      s.loading = false;
    });
  }

  getName() {
    this.showLoading()
    this.names.getName().finally(() => {
      this.hideLoading()
    })
  }
}
const Name = () => {
  const { presenter, state } = usePresenter(IndexPresenter);
  return (
    <div>
      name: {state.name}
      <button onClick={() => {
        presenter.getName()
      }}>getName</button>
    </div>
  );
};

export default Name;

更多的例子

状态库仓库
DI仓库

各位大佬,记得一键三连,给个star,谢谢

本文正在参加「金石计划」