Lellansin/midwayjs-tutorial

6. todolist: 使用依赖注入重写持久化 Service

Opened this issue · 0 comments

MVC 模型

我们这几节课所写的例子已经完成了一个基本的 MVC 模型:

  • model 模型
    • DAO (Data Access Object) 数据访问对象,常用来操作数据库请求
    • Service 公共代码逻辑(包括通过 DAO 操作数据)
  • view 视图
  • cotroller 控制器

其中我们所写的部分与 MVC 对应关系如下:

View Controller Model
HTML直出 HomeController fileDB

请求时序:

image

由于业内流行的 “约定大约配置原则” 我们需要把我们上节课所写的 fileDB.ts 改写成 Midway.js 框架所熟悉的 Service 形式。

使用依赖注入编写一个 Midway Service

Midway 依赖注入的官方文档参见:https://www.yuque.com/midwayjs/midway_v2/container

Midway 的依赖注入由两个装饰器实现,分别是:

  • @provide 直译:vt. 提供。Provide 装饰器可以修饰其随后的 class,将该 class 放入 Midway 的 IoC(依赖注入)容器中。
  • @Inject 直译:vt. 注入。Inject 装饰器可以修饰其随后的 class 身上的属性,在 class 被 new 出来之后会自动从 IoC 容器中取到对应要注入(Inject)的 class 并 new 好实例化赋值在当前 class 的属性上。

原本的 fileDB.ts 代码为:

import { writeFileSync, readFileSync, existsSync } from 'fs';

export let todoList = [];

export function list() {
  if (existsSync('./cache')) {
    const buffer = readFileSync('./cache');
    todoList = JSON.parse(buffer.toString());
  }
  return todoList;
}

export function add(text) {
  todoList.push(text);
  writeFileSync('./cache', JSON.stringify(todoList));
}

export function del(text) {
  const idx = todoList.findIndex((item) => item === text);
  todoList.splice(idx, 1)
  writeFileSync('./cache', JSON.stringify(todoList));
}

重写之后为:

import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';

export const todoList = [];

// ↓ 通过 Provide 装饰器将 TodolistService 放入 IoC 容器中
@Provide('TodolistService')
export class TodolistService {

  list() {
    if (existsSync('./cache')) {
      const buffer = readFileSync('./cache');
      this.todoList = JSON.parse(buffer.toString());
    }
    return this.todoList;
  }

  add(text) {
    this.todoList.push(text);
    writeFileSync('./cache', JSON.stringify(this.todoList));
  }

  del(text) {
    const idx = this.todoList.findIndex((item) => item === text);
    this.todoList.splice(idx, 1)
    writeFileSync('./cache', JSON.stringify(this.todoList));
  }
}

在 Controller 中通过 Inject 注入 TodolistService:

import { Controller, Get, Provide, Inject } from '@midwayjs/decorator';
import { Context } from 'egg';  // egg 中 ctx 的定义
// import * as DB from '../service/fileDB';

@Provide()
@Controller('/')
export class HomeController {
  @Inject('ctx') // 将 ctx 注入到当前 controller 类中
  ctx: Context;

  // Inject 装饰器根据 TodolistService 这个 key 标记当前属性
  // 当 HomeControoler 被实例化的时候,Midway 会从 IoC 容器中
  // 找到 TodolistService 对应的 class 并 new 出来赋值给 this.db
  @Inject('TodolistService')
  db;

  // GET /
  @Get('/')
  async home() {
    // 告诉浏览器,当前返回的是 HTML 页面(而不是纯文本)
    this.ctx.type = 'html';
    const todoList = this.db.list();

    return `
      动态渲染
      <form action="/api/todo" method="POST">
        <input name="text" /> <button>确定</button>
      </form>

      <ul>
        ${todoList.map((item) => `
          <li>
            <form action="/api/todo/delete" method="get">
              ${item}
              <input name="text" type="hidden" value=${item} />
              <button>删除</button>
            </form>
          </li>
        `).join('')}
      </ul>`;
  }
}

单例作用域

默认的未指定或者未声明的情况下,所有的 @Provide 出来的 Class 的作用域都为 请求作用域。这意味着这些 Class ,会在每一次请求第一次调用时被实例化(new),请求结束后实例销毁。

所以如果我们将 todolist 数组放在 TodolistService 的 class 上变成一个 class 成员属性的时候:

import { Provide } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';

@Provide('TodolistService')
export class TodolistService {
  todoList = [];

  list() {
    // ...
  }

  add(text) {
    // ...
  }

  del(text) {
    // ...
  }
}

由于 IoC 的 Class 默认会是请求作用域,也就是在使用的过程中:

// ...

@Provide()
@Controller('/api')
export class APIController {
  @Inject('ctx')
  ctx: Context;

  // 由于默认 IoC 的 Class 默认是请求作用域
  // 每次请求进入时都会 new 一个新的 TodolistService
  // 也就是每次请求进来都会碰到 this.db.todolist = [] 这样重置了内部数组
  @Inject('TodolistService')
  db;

  // POST /api/todo
  @Post('/todo')
  async addTodo() {
    const { text } = this.ctx.request.body;

    // 于是再执行这里时,就会把 [ text ] 写入到 ./cache 文件中
    // 那么不论之前 this.db.todolist 里面有多少内容都会被这个覆盖
    this.db.add(text);

    // 跳转到直出的 HTML 页面
    this.ctx.redirect('/');
    return 'ok';
  }
}

解决方案是使用单例作用域。在 Midway 的依赖注入体系中,有三种作用域。

  • Singleton 单例,全局唯一(进程级别)
  • Request 默认,请求作用域,生命周期绑定请求链路,实例在请求链路上唯一,请求结束立即销毁
  • Prototype 原型作用域,每次调用都会重复创建一个新的对象

不同的作用域有不同的作用,单例 可以用来做进程级别的数据缓存,或者数据库连接等只需要执行一次的工作,同时单例由于全局唯一,只初始化一次,所以调用的时候速度比较快。而 请求作用域 则是大部分需要获取请求参数和数据的服务的选择,原型作用域 使用比较少,在一些特殊的场景下也有它独特的作用。(更多参见 Midway 文档

修改后的代码:

import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';

// ↓ 通过 Scope 装饰器修饰 Provide 出来的 Class
// ↓ 告诉 Midway 框架这个类主需要初始化一次,之后都使用初始化好的
@Scope(ScopeEnum.Singleton)
@Provide('TodolistService')
export class TodolistService {
  private todoList = [];

  list() {
    if (existsSync('./cache')) {
      const buffer = readFileSync('./cache');
      this.todoList = JSON.parse(buffer.toString());
    }
    return this.todoList;
  }

  add(text) {
    this.todoList.push(text);
    writeFileSync('./cache', JSON.stringify(this.todoList));
  }

  del(text) {
    const idx = this.todoList.findIndex((item) => item === text);
    this.todoList.splice(idx, 1)
    writeFileSync('./cache', JSON.stringify(this.todoList));
  }
}

使用 Class 写法的优点

  • 可以将关键变量私有化(private),避免用户直接操作源数据
  • 规范用户通过 class 上的成员方法来增删改查数据
  • 由于隐藏了实现细节,方便重写重构

视频中的代码修改:ac108cd